6 Commits
Author SHA1 Message Date
orrisroot 6d18f7ab4d fix: keep text dark on form controls given a white background
sanitize.css forms.css makes form controls inherit the text color, so
the rules that put them back on white left white text wherever the
surrounding panel sets it, as in the nimgsearch search panel and the
open.neuroinf.jp advanced search.

- set the text to black next to each white control background,
  matching how the browser drew the controls before sanitize.css
2026-09-15 20:02:10 +09:00
orrisroot 6cfc22d252 fix: draw the index tree lines at the right depth
The index tree has a single root, so its first indent unit is always
marked as an end and every line was shifted one column to the right,
doubling the vertical line under the root. Hide that unit and draw the
node's own branch (├ or └) after the indent from the leaf-last class,
as nimg.neuroinf.jp already does.
2026-09-15 18:27:17 +09:00
orrisroot 1402cadecb fix: serve data files with reserved characters in dev and preview
sirv decodes request paths with decodeURI, which leaves %26 (&) and
%23 (#) encoded, so data files with such characters in their names
were not found by the dev and preview servers. Hand sirv the fully
decoded path, the way Apache resolves it in production.
2026-09-14 18:33:37 +09:00
orrisroot 1abc0f9f4b fix: put form controls back on a white background
sanitize.css forms.css makes inputs, selects and buttons transparent.
Give them a white background through the classes and blocks they are
used in.
2026-09-14 16:02:59 +09:00
orrisroot 9dd3492f37 build: switch the base stylesheet to sanitize.css
Use sanitize.css with its forms and typography sheets, as the other
converted sites do, in place of modern-normalize.

- drop normalize-overrides.css and accept the sanitize.css defaults
- give the header search field a white background, which forms.css
  otherwise leaves transparent over the header image
2026-09-14 15:28:53 +09:00
orrisroot e255e8ea4b fix: count the landing page view only once
gtag sends a page_view as soon as it is configured, and the route
effect sent another for the same page.

- pass send_page_view: false when initializing react-ga4
- only track production builds
2026-09-14 13:46:55 +09:00
10 changed files with 92 additions and 57 deletions
+7 -13
View File
@@ -14,7 +14,6 @@
"date-fns": "^4.4.0",
"ky": "^2.1.0",
"lokijs": "^1.5.12",
"modern-normalize": "^3.0.1",
"rc-tree": "^5.13.1",
"react": "^19.3.0",
"react-cookie": "^8.1.2",
@@ -22,6 +21,7 @@
"react-ga4": "^3.0.1",
"react-overlays": "^5.2.1",
"react-router": "^8.3.1",
"sanitize.css": "^13.0.0",
"yet-another-react-lightbox": "^3.32.2"
},
"devDependencies": {
@@ -1535,18 +1535,6 @@
"loose-envify": "cli.js"
}
},
"node_modules/modern-normalize": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-3.0.1.tgz",
"integrity": "sha512-VqlMdYi59Uch6fnUPxnpijWUQe+TW6zeWCvyr6Mb7JibheHzSuAAoJi2c71ZwIaWKpECpGpYHoaaBp6rBRr+/g==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -1869,6 +1857,12 @@
"@rolldown/binding-win32-x64-msvc": "1.2.8"
}
},
"node_modules/sanitize.css": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
"integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==",
"license": "CC0-1.0"
},
"node_modules/scheduler": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
+1 -1
View File
@@ -18,7 +18,6 @@
"date-fns": "^4.4.0",
"ky": "^2.1.0",
"lokijs": "^1.5.12",
"modern-normalize": "^3.0.1",
"rc-tree": "^5.13.1",
"react": "^19.3.0",
"react-cookie": "^8.1.2",
@@ -26,6 +25,7 @@
"react-ga4": "^3.0.1",
"react-overlays": "^5.2.1",
"react-router": "^8.3.1",
"sanitize.css": "^13.0.0",
"yet-another-react-lightbox": "^3.32.2"
},
"devDependencies": {
+4 -2
View File
@@ -9,10 +9,12 @@ import CenterColumn from './CenterColumn';
import Header from './Header';
import LeftColumn from './LeftColumn';
const isAnalyticsEnabled = Config.GOOGLE_ANALYTICS_TRACKING_ID !== '';
const isAnalyticsEnabled = import.meta.env.PROD && Config.GOOGLE_ANALYTICS_TRACKING_ID !== '';
if (isAnalyticsEnabled) {
ReactGA.initialize(Config.GOOGLE_ANALYTICS_TRACKING_ID);
// Page views are sent from the location effect below, the first one included;
// gtag's own page_view on config would count the landing page twice.
ReactGA.initialize(Config.GOOGLE_ANALYTICS_TRACKING_ID, { gtagOptions: { send_page_view: false } });
}
const toMultiLang = (value: unknown): MultiLang | null => (value === 'en' || value === 'ja' ? value : null);
+10
View File
@@ -177,6 +177,8 @@ form {
border: 1px solid #eeeeee;
border-style: inset;
padding: 2px;
/* sanitize.css leaves form fields transparent, which vanishes on the header image */
background-color: #ffffff;
/* border-box: 170 + 2px padding + 1px border per side, likewise 16 high */
width: 176px;
height: 22px;
@@ -614,3 +616,11 @@ div.xoopsQuote {
padding: 5px 0 5px 28px;
background-color: inherit;
}
/* sanitize.css forms.css leaves form controls transparent; keep the search
and language blocks on white. */
.leftcolumn .blockContent input[type="text"],
.leftcolumn .blockContent select {
background-color: #ffffff;
color: #000000;
}
+7
View File
@@ -195,6 +195,13 @@ form.d3f_form p input {
vertical-align: middle;
}
/* sanitize.css forms.css leaves form controls transparent; keep them on white. */
form.d3f_form select,
form.d3f_form input[type="submit"] {
background-color: #ffffff;
color: #000000;
}
/* d3f_ctrl */
.d3f_ctrl {
width: 100%;
+10
View File
@@ -68,3 +68,13 @@
.database :global(.advancedSearch .fieldDate input) {
margin: 0 5px 0 0;
}
/* sanitize.css forms.css leaves form controls transparent; keep them on white. */
.database :global(.fieldInputText),
.database :global(.fieldSelect),
.database :global(.fieldDate select),
.database :global(.fieldDate input),
.database :global(.listTable select) {
background-color: #ffffff;
color: #000000;
}
+22 -8
View File
@@ -16,6 +16,9 @@
.formButton {
margin: 3px 3px 10px;
/* sanitize.css forms.css leaves buttons transparent */
background-color: #ffffff;
color: #000000;
}
.indexTree div span {
@@ -41,26 +44,37 @@
display: inline-block;
height: 20px;
width: 9px;
background: url(../assets/images/tree_line.png);
background: url(../assets/images/tree_line.png) no-repeat -18px 0;
}
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-indent .rc-tree-indent-unit:not(:last-child)) {
/* treeData has a single root, so the first indent unit never carries a line. */
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-indent .rc-tree-indent-unit:first-child) {
display: none;
}
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-indent .rc-tree-indent-unit:not(.rc-tree-indent-unit-end)) {
background-position: -9px 0;
}
.indexTree:global(
.rc-tree .rc-tree-treenode .rc-tree-indent .rc-tree-indent-unit:not(:last-child).rc-tree-indent-unit-end
) {
background-position: -18px 0;
/* The node's own branch (├ or └) is drawn after the indent units. */
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-indent)::after {
content: "";
display: inline-block;
vertical-align: bottom;
height: 20px;
width: 9px;
background: url(../assets/images/tree_line.png) no-repeat 0 0;
}
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-indent .rc-tree-indent-unit:last-child.rc-tree-indent-unit-end) {
.indexTree:global(.rc-tree .rc-tree-treenode.rc-tree-treenode-leaf-last .rc-tree-indent)::after {
background-position: -27px 0;
}
.indexTree:global(.rc-tree .rc-tree-treenode:first-child .rc-tree-indent)::after {
display: none;
}
.indexTree:global(.rc-tree .rc-tree-treenode .rc-tree-switcher) {
display: inline-block;
height: 20px;
width: 16px;
margin-right: 2px;
background: url(../assets/images/tree_node.png);
background: url(../assets/images/tree_node.png) no-repeat;
cursor: pointer;
}
.indexTree:global(.rc-tree .rc-tree-treenode:first-child .rc-tree-switcher.rc-tree-switcher-noop) {
+3 -2
View File
@@ -1,7 +1,8 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import 'modern-normalize/modern-normalize.css';
import './normalize-overrides.css';
import 'sanitize.css';
import 'sanitize.css/forms.css';
import 'sanitize.css/typography.css';
import App from './App';
import CreditsUtils from './credits/lib/CreditsUtils';
import D3ForumUtils from './d3forum/lib/D3ForumUtils';
-30
View File
@@ -1,30 +0,0 @@
/*
* modern-normalize changes fonts the XOOPS theme was not written for. The theme
* styles `body` and leaves the rest to the defaults it was authored against,
* so restate those defaults as explicit values here rather than leaving the
* rendering to depend on whichever browser is in use.
*
* Its `box-sizing: border-box` is kept -- it is the easier model to reason
* about -- and the handful of theme rules that were written as content-box
* arithmetic now state their border-box totals instead.
*/
/* modern-normalize puts a system font stack on `html`. Nothing visible
inherits it -- `body` below covers the whole document -- but state the
theme's own stack so there is a single font declaration for the page. */
html {
font-family:
Verdana, Helvetica, "MS Pゴシック", "Hiragino Kaku Gothic Pro", "ヒラギノ角ゴ Pro W3", osaka, sans-serif;
}
/* modern-normalize makes form controls inherit the page font. The theme sizes
them as the unstyled controls they were: Arial at the control default. */
button,
input,
optgroup,
select,
textarea {
font-family: Arial, sans-serif;
font-size: 13.3333px;
line-height: normal;
}
+28 -1
View File
@@ -20,7 +20,34 @@ const staticContents = (): Plugin => {
// browser revalidates against the ETag instead of falling back to
// heuristic caching. Without it a re-dump, or another site served
// from the same localhost port, is answered from a stale cache.
middlewares.use(sirv(dir, { dev, etag: true, maxAge: 0 }));
const serve = sirv(dir, { dev, etag: true, maxAge: 0 });
middlewares.use((req, res, next) => {
// sirv decodes the path with decodeURI, which leaves reserved
// characters such as %26 (&) and %23 (#) encoded, so a file like
// `CFP&Reg.png` is not found. sirv reuses `req._parsedUrl` when
// it was parsed from the same `req.url`, so hand it the path
// decoded the way Apache resolves it.
const raw = req.url ?? '/';
const idx = raw.search(/[?#]/);
const pathname = idx === -1 ? raw : raw.slice(0, idx);
if (pathname.includes('%')) {
try {
const decoded = pathname.split('/').map(decodeURIComponent).join('/');
if (!decoded.includes('%')) {
(req as typeof req & { _parsedUrl?: object })._parsedUrl = {
pathname: decoded,
search: '',
query: undefined,
hash: undefined,
raw,
};
}
} catch {
// leave malformed escapes to sirv
}
}
serve(req, res, next);
});
}
};
return {