Files
5chan/src/components/reply-modal/reply-modal.module.css
T
Tommaso CasaburiandGitHub 17c63bb2e6 feat(reply modal): add sci tex preview button (#1170)
* feat(sci): add 4chan-style TeX support with MathJax on /sci/

- [math]/[eqn] tags typeset with MathJax 3 (lazy chunk, only on /sci/ with math present)
- 4chan-identical config: Safe mode, left-aligned eqn, neutered \color/\newcommand macros
- TeX button in reply modal title bar opens live TeX Preview modal
- /sci/ post form rules bullets for [math]/[eqn] usage and right-click source
- MathJax context menu on right-click (Show Math As > TeX Commands)
- woff fonts served from node_modules in dev and emitted into build

* feat(sci): finish TeX support: configmacros fix, preview preload, translations, tests

- add configmacros package so the 4chan macro neutering (\color, \newcommand, ...) applies
- preload MathJax when the TeX Preview opens, like 4chan
- stable closeModal callback for the preview modal
- pre-bundle mathjax components in vite optimizeDeps to avoid dev mid-session reload
- translate the 6 new TeX keys into all 35 languages
- markdown math segment component tests + math-tags unit tests

* feat(reply modal): add sci tex preview button

* fix(tex-preview): clear MathJax bookkeeping and pending typeset on close

Addresses Cursor Bugbot: the preview output was typeset via typesetMathElement but
never passed to clearMathElement on unmount, so repeated open/close cycles kept
detached nodes in MathJax's math list. Also cancels the pending debounce timer.

* fix(reply-modal): reset TeX preview state when the reply modal closes

Addresses CodeRabbit: showTexPreview persisted across close/reopen like the
bbcode preview flags, so the TeX preview would auto-open on the next reply.
2026-06-11 16:12:38 +07:00

235 lines
4.4 KiB
CSS

.container {
position: fixed;
display: block;
padding: 2px;
font-size: 10pt;
border-left: none;
border-top: none;
z-index: 999;
background-color: var(--challenge-modal-background-color);
border: var(--challenge-modal-border);
}
.title {
font-size: var(--challenge-modal-title-font-size);
text-align: center;
margin-bottom: 1px;
padding: 0;
height: 18px;
line-height: 18px;
cursor: move;
font-weight: var(--challenge-modal-title-font-weight);
background-color: var(--challenge-modal-title-background-color);
color: var(--challenge-modal-title-text-color);
border: var(--challenge-modal-title-border);
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
.closeIcon {
all: unset;
float: right;
cursor: pointer;
margin-bottom: -4px;
width: 18px;
height: 18px;
border: none;
image-rendering: pixelated;
background-image: var(--close-button-background-image);
}
.texButtonTooltip {
float: left;
display: block;
height: 18px;
line-height: 18px;
margin: 0 0 0 3px;
}
.texButton {
all: unset;
display: block;
cursor: pointer;
color: var(--button-desktop-text-color);
font-size: 10.6667px;
font-weight: 700;
line-height: 18px;
text-decoration: none;
}
.texButton:hover,
.texButton:focus-visible {
color: var(--button-desktop-text-color-hover);
text-decoration: none;
}
.texButton:focus-visible {
outline: 1px dotted currentColor;
outline-offset: 1px;
}
.texButton sub {
font-size: 80%;
pointer-events: none;
}
.container input[type="text"], .container textarea, .container select:not(.flagSelector) {
border: var(--reply-modal-field-input-border, revert);
font-family: var(--post-form-field-font-family, revert);
appearance: var(--post-form-field-input-appearance, revert);
outline: var(--post-form-field-input-outline, revert);
background-color: var(--post-form-field-input-background-color, revert);
color: var(--post-form-field-input-color, revert);
font-size: 10pt;
outline: var(--post-form-field-input-outline, revert);
width: 298px;
padding: 2px;
margin-bottom: 1px;
}
.container input[type="text"]:focus, .container textarea:focus, .container select:not(.flagSelector):focus {
border: var(--reply-modal-field-input-border-focus, revert);
}
.content {
position: relative;
}
.content textarea {
vertical-align: top;
}
.footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.footer button {
text-transform: capitalize;
float: right;
cursor: pointer;
}
.footer input[type="checkbox"] {
margin: 0 3px;
vertical-align: text-top;
}
.spoilerButton {
display: inline-block;
padding-top: 2px;
}
.spoilerButton label, .spoilerButton input[type="checkbox"] {
cursor: pointer;
}
.publishButton {
width: 75px;
filter: var(--filter80);
}
.uploadButton {
float: left;
margin-right: 5px;
white-space: nowrap;
}
.uploadButton button {
white-space: nowrap;
}
.uploadFileName {
display: inline-block;
width: calc(100% - 75px);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.uploadContainer {
display: inline-flex;
align-items: center;
width: 130px;
}
.oekakiRow {
display: flex;
align-items: flex-start;
width: 302px;
margin-bottom: 1px;
font-size: 10pt;
}
.oekakiLabel {
display: inline-block;
width: 38px;
padding-top: 3px;
font-weight: bold;
}
.oekakiControls {
width: 264px;
}
.oekakiWarning {
width: 294px;
margin-bottom: 1px;
font-size: 11px;
line-height: 1.2;
}
.offlineBoard {
width: 292px;
font-family: monospace;
background-color: #E62020;
font-size: 12px;
color: white;
padding: 3px 7px 3px 5px;
text-shadow: 0 1px rgba(0, 0, 0, 0.20);
}
@media (max-width: 640px) {
.container {
position: absolute;
}
.title {
cursor: default;
}
.container input[type="text"], .container textarea, .container select:not(.flagSelector) {
font-size: 16px;
}
}
.error {
width: 294px;
font-family: monospace;
background-color: #E62020;
font-size: 12px;
color: white;
padding: 3px 5px;
margin-top: 1px;
text-shadow: 0 1px rgba(0, 0, 0, 0.20);
}
.error a,
.error a:visited {
color: inherit;
text-decoration: underline;
}
.status {
width: 294px;
font-family: monospace;
background-color: rgba(0, 0, 0, 0.08);
font-size: 12px;
color: var(--post-link-text-color);
padding: 3px 5px;
margin-top: 1px;
overflow-wrap: anywhere;
}