Skip to content

[3.0] Theme - #7933

Closed
live627 wants to merge 152 commits into
SimpleMachines:release-3.0from
live627:theme
Closed

[3.0] Theme#7933
live627 wants to merge 152 commits into
SimpleMachines:release-3.0from
live627:theme

Conversation

@live627

@live627 live627 commented Dec 4, 2023

Copy link
Copy Markdown
Contributor

This is the new theme started by @TwitchisMental

I still need to convert several form grids to use CSS grids instead of floats. Also the grids abuse definition list markup, something tat has always annoyed me from the very beginning.

Submitting as a draft now to get early feedback.

@live627 live627 added the Theme label Dec 4, 2023
@Sesquipedalian Sesquipedalian added this to the 3.0 Alpha 4 milestone Dec 4, 2023
Comment thread Themes/default/css/index.css
Comment thread Themes/default/css/custom.css Outdated
Comment thread Themes/default/index.template.php Outdated
Comment thread Themes/default/index.template.php Outdated
echo User::$me->avatar['image'];

echo '<span class="textmenu">', User::$me->name, '</span></a>
echo '</a>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do this for the avatar image, shouldn't we use a alt text for the name?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use href or url then.
Or should 'image' provide it by default? It would be good in case an external avatar is missing in other parts of the forum.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either is valid. I believe it just needs something to be valid for screen readers. Since its a link to the users profile and we removed the name, the alt should have it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I think 'image' should provide the alt text by default. An external avatar could suddenly not load or disappear, would be good to have it.

Comment thread Themes/default/scripts/register.js
@live627

live627 commented Dec 28, 2023

Copy link
Copy Markdown
Contributor Author

I notice that a few forms would disable textareas on submit, such as the posting form. I've changed this to do all forms by using the new HTML property to make the entire form inert.

@live627
live627 force-pushed the theme branch 2 times, most recently from 3ba4ff5 to c823a49 Compare December 28, 2023 06:58
@dragomano

Copy link
Copy Markdown
Contributor

Icons in the admin area are not indented enough:
sshot-11

Unable to load the '' template at Forum Maintenance - Routine:
sshot-12

SCEditor doesn't have a dark theme?
sshot-13

Instead of "X Posts" and "Y Topics", maybe it would still be better to keep "Posts: X" and "Topics: Y" as it is now?
sshot-14

@DiegoAndresCortes

Copy link
Copy Markdown
Member

Icons in the admin area are not indented enough: sshot-11

I’m working on this

SCEditor doesn't have a dark theme? sshot-13

Can be fixed later too, but also requires some tweaks.
Check this topic: https://www.simplemachines.org/community/index.php?topic=586626.0

Instead of "X Posts" and "Y Topics", maybe it would still be better to keep "Posts: X" and "Topics: Y" as it is now? sshot-14

I don’t have a preference, we might discuss it later for sure.

@live627
live627 force-pushed the theme branch 2 times, most recently from 2322f68 to ada494b Compare December 29, 2023 03:14
@jdarwood007 jdarwood007 changed the title Theme [3.0] Theme Jan 20, 2024
@jdarwood007

Copy link
Copy Markdown
Member

@live627 Can you rebase this or merge and fix conflicts?

@DiegoAndresCortes

Copy link
Copy Markdown
Member

We have this pending
live627#44

Will start sending more in upcoming weeks, but it could be merged, rest of changes are more focused so can be in separated PR's

@live627

live627 commented Jan 30, 2024

Copy link
Copy Markdown
Contributor Author

Once I can get around to testing and merging that, I'll rebase this branch again

Comment thread Themes/default/Reminder.template.php Outdated
<div class="windowbg form_grid">
<p class="descbox">', Lang::$txt['authentication_options'], ':</p>
<p>
<input type="radio" name="reminder_type" id="reminder_type_email" value="email" checkeiv></label>

@dragomano dragomano Feb 9, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkeiv => checked?

Comment thread Themes/default/Register.template.php Outdated
<input type="submit" value="', Lang::$txt['save'], '" name="save_reserved_names" tabindex="', Utils::$context['tabindex']++, '" class="button">
<div class="form_grid">
<div>
<label for="matchword">', Lang::$txt['admin_match_whole'], '</div>

@dragomano dragomano Feb 9, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<label>...</div> => <label>...</label>?

@live627

live627 commented Feb 20, 2024

Copy link
Copy Markdown
Contributor Author

@jdarwood007 I moved some of the login javascript to its own file but don't know how to test cors requests.

@jdarwood007

Copy link
Copy Markdown
Member

When this is closer to ready I can check it. You need to run 2 domains (or 2 subdomains) and have the forum on one and the script on another. A simple SSI page where you can open the user area popup is good enough for most things. Sending credentials such as on the login form may also need to be tested, but when you can open the user area, the CORS request works.

@albertlast

Copy link
Copy Markdown
Collaborator

Wave 4, day two — parts 11 to 33, and the thing that is now actually in the way

Picking up from the status a day ago. Wave 4 has gone from ten parts to thirty-three, and half the areas are now closed out rather than merely started.

Theme parts since the last update

Area Parts
Posting form #9428 (merged)
Poll editor, login ajax, search board picker #9429 #9430 #9432
Profile scripts #9436 #9437 #9438 #9441
calendar.css tokens #9443
Long tail #9445
Admin centre #9447 #9448
Board index / message index #9451
Editor leftovers #9457
index.css tokens, nine slices #9467 #9468 #9469 #9470 #9471 #9472 #9473 #9474 #9475

55 theme PRs in total, 33 merged, 22 open.

The nine index.css slices need a word, because they are not what wave 2 did. I measured the two versions of that file properly before starting: release-3.0 has 954 top-level selectors and this branch has 776, of which only 470 are shared — and of those, only 305 are byte-identical once the tokens are resolved back to literals. This branch's index.css also uses 1192 distinct custom properties, 962 of which variables.css does not define, because it mints roughly one token per property per component (--autosuggest-item-cursor, --border_width). Porting that wholesale would be a single PR adding a thousand tokens, which is the least reviewable thing in this branch.

So the slices take this branch's structure and naming and release-3.0's values: 120 of the 188 remaining literals in index.css become var(--…), and nothing on screen moves. The acceptance test for each is mechanical — parse every rule out of the raw stylesheet text, apply each to a probe element, read a fixed list of 57 longhands, and diff against the same capture taken on release-3.0. That is 965 rules and 55,005 computed values per slice, all nine at zero differences. (Three traps in doing that, in case anyone repeats it: the CSSOM cannot round-trip a border shorthand containing a var(), so reading longhands off CSSRule.style invents confident, entirely fake differences; minimize_files has to be 0, because the minifier rewrites literal transparent to rgba(255,255,255,0) and cannot do so inside a var(); and the page you measure on has to be one that returns 200 on the base branch — see below for why that is a smaller set than you would expect.)

The bug count, again — and why I think it is now the real bottleneck

Since the split started on 1 August: 70 non-theme bug PRs, 17 merged, 53 still open. Yesterday that number was 23. Today alone it went up by about twenty.

I am not raising this as a complaint about review speed. I am raising it because the open queue has started to block the split itself, in a specific and repeatable way:

To sweep the topic display at all today I had to build a scratch branch of release-3.0 + #9405 + #9459 before a single page rendered. The same is true in miniature elsewhere: #9432 blocks the shared board picker, and the profile markup renames cannot land until profile.css and its tokens do.

There is also a self-inflicted lesson in here that is worth stating publicly, because it nearly cost me: I measured six of the CSS slices on ?topic=1.0 without noticing it was the #9405 fatal. The measurements happened to survive — an error page still loads variables.css, and base and branch were captured in the same state — but that was luck, not method, and the same mistake produced one false bug report on #9469 that I have since retracted. Every capture now records the URL and whether the page is a fatal, and the comparer refuses to run on a mismatch.

Areas now closed out

Five of the ten areas from the plan are done, in the sense that everything portable has been taken and everything left has a written reason. (Profile and the admin centre are done apart from one CSS-coupled bundle each; those are in the table further down.)

A pattern worth naming, since it accounts for most of today's finds: 3.0's type declarations turn survivable 2.1 bugs into 500s. A string from COUNT(*), a null left by a loop that never ran, a typed property with no default reached through a switch with no default — all of these ran fine untyped. Anything in 3.0 that reaches a typed signature from the database, from $_GET, or from a loop that may not execute is worth a second look, and the guard clause that was meant to catch it is often placed after the call that now throws.

Worth recording against yesterday's estimate table, which put topic display at 4–6 theme parts: it produced none. That is the third area running where the honest answer was "the diff is reverts, here is what running the pages found instead", and I now expect it to be the normal outcome for the remaining template areas rather than the exception. git diff release-3.0 theme-ref --stat reads 145 files today, up from 131, for the reasons given last time — release-3.0 keeps gaining files this branch does not have, several of them because of this split. Per-area is still the only honest way to read progress.

What is still open in this PR

Two areas, plus the CSS-coupled bundles that cut across them:

Left to do Why it has not been done
Login / Register / Reminder — the data-autov rework Coupled to register.js. The Login half needs rewriting rather than porting: this branch's Login.template.php calls new smf_Login when the class is SMF_Login, passes sCors and bIsFromAjax unquoted, and drops the error handler; its login.js omits the X-Requested-With header that Login2::checkAjax requires.
Profileprofile.css + --basicinfo-* tokens + the dependent renames Must land as one PR. profile.css does not exist on release-3.0 and the tokens are not in variables.css, so porting the renames first would delete the styling.
Admin centretemplate_admin() tiles + template_show_settings().settings-grid Neither works without the admin.css restyle, and show_settings touches every admin settings page.
Board index — the markup restyle Still the Font Awesome blocker: .board_icon a::before draws its glyphs from a font whose @font-face rules the shipped fontawesome.min.css does not contain. Its own issue, not a slice of this.
Message index — extracting template_list_topics() Removes no duplication on its own and has no user-visible effect; it only makes sense travelling with the index.css slice, and would collide with #9454.
index.css remainder The gradient block near the file tail, the profile and stats rules, and the register/error-file forms.
attachments.css slice Carries the #au-template fix — a <label for="attached_BBC_width"> over an input with no id, which cannot be given one because dropzone clones the template once per file. The only correct fix wraps the input, which moves the column, which makes it a restyle.
Wave 2 leftovers dark.css and icons.css content, RTL to logical properties.
Editor / SCEditor remainder Deferred from wave 1.

Also recorded rather than shipped, so they are not lost: .approvebg2 is emitted nowhere in the codebase (belongs with the index.css slice); QuickModifyTopic — inline subject editing on the message index — is dead three separate ways and needs its ajax round trip verified rather than half-fixed inside a markup PR; and a whole-theme audit of floats inside flex bars is owed after #9390, which a text search cannot do because the float comes from the stylesheet, not a class.

Next session

Login / Register / Reminder, which is the last unstarted area, then the CSS-coupled bundles in the order Profile → Admin → index.css remainder, since each of those is one PR that cannot be subdivided further without breaking the page it belongs to.

The single most useful thing anyone else could do is #9405 and #9459. Both are small, both make an entire area of the forum work again, and both are currently on the critical path for the rest of this split.

Attribution unchanged: this is @live627's work, with @TwitchisMental, @DiegoAndresCortes, @sbulen and @jdarwood007 on the branch. The split is only a delivery mechanism for it.

@albertlast

Copy link
Copy Markdown
Collaborator

Wave 4 complete — per-area template groups

Wave 4 covered the second of the three groupings asked for on this PR: template changes
grouped by area. All ten areas have now been taken to exhaustion, which here means every
remaining hunk in each one is either shipped as a part or written down with the reason it
was refused.

34 parts, #9382 through #9487. Parts 1–11 are merged; 12–34 are open.

The parts, by area
Area Parts
Calendar #9382, #9443
Board index and message index #9385, #9403, #9451, #9475
Admin centre #9386, #9388, #9404, #9447, #9448
Shared chrome carried in from wave 3 #9390, #9391, #9392, #9393
Posting #9428, #9429, #9457
Search #9432
Profile #9436, #9437, #9438, #9441
Long tail #9445
Login, registration and reminders #9430, #9487
index.css token slices #9467#9475

What the wave turned up

Most of what is left in the snapshot is older than release-3.0. The usual reason for
refusing a hunk stopped being "this is hard to separate" and became "this is a revert". Whole
files in the snapshot predate work that has since landed here: the calendar sources predate
both the clock rework and the fatalLang(…, status) signature, several templates undo the
shared confirmation handler and the auto-submit fix, and one moderation template introduces
copy-paste bugs that release-3.0 does not have. Each of those is recorded with its specific
evidence rather than skipped quietly, because "this hunk is a revert, here is the proof" is a
result and not a shortfall.

The raw diff has stopped being a progress measure. git diff release-3.0 <snapshot>
currently reads 145 files, which is more than the 110 this started at — the snapshot is 161
commits behind, so release-3.0's own new work counts as difference. Split directionally it
is 101 files the snapshot itself changed and which still differ, and 44 that differ only
because release-3.0 moved on.

The end condition needs restating too. That diff will never reach zero. Roughly forty
hunks have now been refused with a reason and will sit in it permanently. This PR is finished
when every remaining hunk is either shipped or recorded as rejected — not when the diff is
empty.

Two blockers surfaced, both outside the split. The bundled Font Awesome cannot render:
Themes/default/webfonts/ holds only index.php, and fontawesome.min.css carries no
@font-face, so fontawesome_source = local loads 57 KB of stylesheet backed by no font
files. That is what the board index markup and the personal message popup header are waiting
on, and fixing it is new work rather than a slice of this PR. Separately, dark.css cannot be
ported: the snapshot's copy is pure token repointing, but only 78 of its 197 custom properties
exist in release-3.0's variables.css, and 198 of release-3.0's 276 tokens are not
repointed at all, 87 of them colour-bearing. Taken as-is it would give a dark mode with 87
holes still showing their light values.

The areas whose diff yielded nothing still produced work. Posting and personal messages
had no portable hunk between them — everything was either newer here or a revert — but running
those pages and operating the widgets on them found faults that no diff would have shown.
Those went out as ordinary bug fixes rather than as parts of this split, which is why the part
numbers do not account for the wave's whole output.

Wave 5

What remains of this PR is, by construction, the visual restyle: everything that could land
without a visible change has now been taken. So wave 5 inverts the review criterion. Waves 2
and 4 could be reviewed as "prove nothing moved"; wave 5 has to be "here is what changes, and
why", one area at a time.

Planned for it:

  • The CSS-coupled bundles, one PR per area, with the template, its stylesheet slice and
    its tokens together — none of them renders correctly apart. That is profile, the admin
    centre (where template_show_settings() becoming a settings grid touches every admin
    settings page), and login/registration/reminders.
  • A working local Font Awesome, which unblocks the board index and the PM popup header.
  • Dark mode written against release-3.0's own token set, rather than ported.
  • The index.css remainder — the gradient block near the file tail, the profile and stats
    rules, and the register and error-file forms.
  • The attachments.css and postbit.css slices, which carry two fixes deliberately left
    out of a markup PR because each needs a layout change to do properly.

One note on sequencing. Wave 5's bundles edit the same files as wave 4's open parts, so
starting it before those land would mean rebasing every one of them. Wave 5 begins once wave 4
has merged, and its scope will be remeasured against release-3.0 at that point rather than
against today's snapshot.

live627 pushed a commit that referenced this pull request Aug 10, 2026
register.js worked out what each field was by reading its id and looking for
substrings in it, so a field had to be called smf_autov_pwmain and the template
had to write out two nested spans called smf_autov_pwmain_div and
smf_autov_pwmain_img for it to draw an icon into. Nothing outside this file ever
addressed any of those.

The field now carries data-autov, and the icon is built where it is used. That
takes twelve lines of markup out of the two templates and the id-sniffing out of
the script, and leaves the ids free to be about the labels, which is what
Register's are now for.

Rendering is unchanged. The icons are the same main_icons check, valid and
invalid spans in the same place, the inputs still take valid_input and
invalid_input, and the username icon is still the link that asks the server.

Two things that fell out of it:

The blur handler that asks whether a username is taken was attached to every
verified field, so typing a password fired a username lookup. It is the
username's now.

refreshMainPassword() took a flag whose only purpose was to stop it and
refreshVerifyPassword() calling each other forever. The rule itself is now a
function both of them ask, so neither has to know about the other.

Part of the #7933 split.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
@albertlast

Copy link
Copy Markdown
Collaborator

Wave 5 complete — the visual restyle begins, and dark mode lands

Wave 5 was the first wave whose review criterion is "here is what changes, and why" rather
than "prove nothing moved". It ran to three parts, #9496#9498, all open.

Part
1 #9496 The last hard-coded colours in index.css get a token
2 #9497 A dark mode for the default theme
3 #9498 The colour mode reaches the post editor

They are stacked in that order, because each one is what makes the next possible.

One rule the wave established

A change that must prove nothing moved and a change that must show what moved do not belong
in the same pull request.
Dark mode started as a single branch; the first screenshot of it
had a white frame around the whole page and headings rendering dark-on-dark, because 55
declarations in index.css still held literal colours that no override could reach. Folding
that fix into the dark-mode branch would have left no usable criterion for either half. So
part 1 is the tokenising, verified as a strict no-op, and part 2 is the mode, verified by
looking at it.

Dark mode was broken in four places, not one

The plumbing has been in 3.0 since Alpha and none of it had ever run. Turning the flag on was
not enough. Each of these was found by using the feature, not by reading it:

  • Theme::loadMode() had no callers at all. It is the method that resolves the mode and
    loads dark.css, so data-mode was never emitted and the stylesheet never requested.
  • system mode had lost the thing that makes it system. That mode works by loading
    dark.css with a (prefers-color-scheme: dark) media attribute, but attributes are only
    printed for files served on their own, and loadCSSFile() minimizes by default — folded
    into the bundle, the attribute disappeared. The browser was never asked, so system would
    have meant always dark.
  • Saving the chosen mode to a member's profile was fatal. Db::insert() wants a list of
    rows and was handed a single flat one, which outside backward-compatibility mode is a
    critical error. The first time a logged-in member picked a mode, the page died.
  • dark.css was the placeholder.

loadVariant() carries the identical Db::insert() defect three lines below the one fixed
here. It is deliberately untouched: that branch neither enables nor exercises variants, and
shipping an unverified fix is worse than recording a known one.

The stylesheet is written, not ported

The snapshot's dark.css could not be used, and the gap had widened since wave 4 added its
tokens: of its 197 overrides only 84 name a token that exists here, while 213
colour-bearing tokens here have no value in it. Taken as-is it would have produced a dark mode
with most of the forum still at its light values.

What is there instead overrides 243 tokens, restates no rules, and is written against
twelve values declared at the top of the file — five surfaces, two hairline weights, three
text weights, two link colours. Agreeing those twelve is most of the review. The colour ramps
are deliberately untouched, so a theme still re-tints by moving --primary-color-hue and dark
mode moves with it.

How it was checked

  • Light mode is unchanged, three times over: the computed colour, background, background
    image, four border colours, box shadow, outline, text shadow — and for part 3 also filter
    and fill — of every element across twelve pages, before and after. 0 differences, each
    time.
  • Eighteen pages swept in both modes for text landing on a background too close to it.
    Dark mode has 19 elements below the AA contrast ratio against light mode's 136; by
    that measure the new mode reads better than the shipped one. Both dark causes sit outside
    the theme: the bundled coloris colour picker, which carries no tokens at all, and the
    Administrator group's online_color, which is data rather than styling.
  • No errors logged during any of it.

One methodological trap worth passing on: a computed-style comparison is sensitive to forum
state, not only to CSS. One run reported 4132 differences that were entirely false — an
alert badge count had changed between two captures taken hours apart, shifting every element
index. The tell is a changed element count and misaligned tags. Capture both sides back to
back.

The editor, and where the mode stops

Part 3 found the last light island: the editor's own stylesheet had never been tokenised, so
the toolbar sat in the middle of a dark page in light grey. Twenty-four literals, now none.

One of its tokens is not a colour. Every toolbar icon is cut from a single raster sprite, so
no rule can recolour them, and in dark mode the near-black line art all but vanished. A filter
is the only lever a colour mode has over a raster, so the sprite gets one — none in light,
an inversion paired with a hue rotation in dark. The rotation is required because the sprite
is mixed: the greys have no hue and simply invert, while the icons that carry their own colour
come back near where they started.

The mode stops at the iframe. The surface you type on is a separate document, and
Editor.php hands it one stylesheet with no tokens in scope. Tokenising that file would leave
every var() unresolved and break the light theme too, so feeding the iframe its tokens is
a change to how the editor is given stylesheets and belongs on its own. It is narrower than it
sounds: the rich surface needs a per-member option that is off by default, so what everyone
meets first is the source view — an ordinary textarea on the page, which follows the mode
correctly.

Things that turned out not to be true

Two items this PR has carried for a while do not survive contact:

  • Font Awesome is not blocking anything today. There are zero fa- classes anywhere
    in Themes/ or Sources/. It is loaded on every page and used by nothing. The local
    option is still broken — no @font-face, and webfonts/ holds only an index.php — but
    that gates future ports, not anything that renders now. Anyone fixing it should know the
    snapshot's bundled webfonts are 6.3.0 while its stylesheet is 7.1.0, so they cannot
    simply be lifted across.
  • "Non-image attachments show the width and height fields" is not reproducible. Tested
    with a text file: the block is present but hidden, because the stylesheet hides it by
    default and only the image branch reveals it.

While testing that, something else surfaced that is worth flagging on its own: every
attachment upload on release-3.0 fails
, with Unknown column 'm.id_topic' in 'field list'
from Sources/Attachment.php:537. That is #9459, still open. Any attachments.css or
postbit.css work needs it in place first, or the success path never runs at all.


Wave 6 — profile

The profile area is the next wave on its own, because it is bigger than it looks and the
groundwork is a trap rather than a formality.

The obvious first move — lift the profile rules out of index.css into a profile.css — is
not the safe no-op it appears to be:

  • That block opens with a global rule, a bare dl { overflow: auto; } sitting under the
    section header. Moving the block changes every page in the forum that has a dl.
  • The selectors under that header are not profile-scoped. .list_posts is used by the
    moderation centre, personal messages, posting, recent posts and search; #tracking by the
    topic display, the editor controls, personal messages and Who's Online; #creator by the
    password reminder.

And the snapshot's own profile.css cannot be ported. It is loaded only from
Sources/Actions/Profile/Main.php, yet it claims #paid_subscription, which the admin
paid-subscriptions template emits, and #creator, which the password reminder page emits.
Both pages would lose those rules outright. It does correctly leave .list_posts behind — so
this is a per-selector problem, not a blanket one, which is exactly why it needs per-rule
triage instead of a block move.

So wave 6 re-derives the file from release-3.0, checking every selector against the
templates that actually emit it before moving it, and only then takes the template renames
that depend on it — the post and list-item classes, and the counter — which cannot land before
the stylesheet exists without deleting their own styling.

Wave 7 — what is left after that

Named now so the shape is visible, not scheduled:

  • The admin centre settings grid. dl.settings is not the small conversion it looks like
    either: it appears in more than twenty templates rather than only in
    template_show_settings(), it is float-based, and it tolerates mismatched term and
    definition counts in a way a grid does not. Doing it in CSS alone on the existing class
    would avoid renaming it across every one of those templates and avoid breaking mods that
    target it — but it still has to be checked on every admin settings page.
  • Login, registration and reminders. The forms move onto a grid class release-3.0 does
    not define at all, so the templates and the stylesheet slice have to land together. The
    snapshot's own login template needs rewriting rather than porting; the specifics were
    catalogued during wave 4.
  • The attachments.css and postbit.css slices, once [3.0] Keep the messages join Attachment::load() selects from #9459 makes uploads work again.
  • Tokens for the editor's iframe, which is the one place the colour mode still cannot
    reach.

The standing note from wave 4 applies unchanged: what remains of this pull request is the
visual restyle, and the raw diff stopped being a progress measure some time ago. This closes
when every remaining hunk is either shipped or recorded as rejected with its reason — not when
the diff is empty.

jdarwood007 pushed a commit that referenced this pull request Aug 23, 2026
The colour mode plumbing has been in 3.0 since Alpha: Theme.php resolves the
mode, Themes.template.php and ThemeOptions.php offer it to the admin and the
member, index.template.php writes it into data-mode, and dark.css has sat in the
theme as a placeholder. None of it has ever run. Turning the flag on was not
enough, because the feature is broken in four places rather than one.

loadMode() had no callers. It is the method that resolves the mode and loads
dark.css, and nothing anywhere invoked it, so data-mode was never emitted and
the stylesheet was never asked for. It is now called beside loadVariant(), which
is its sibling and is read by the same line of the template.

The 'system' mode lost the very thing that makes it 'system'. That mode works by
loading dark.css with a (prefers-color-scheme: dark) media attribute, but
attributes are only printed for files served on their own - loadCSSFile()
defaults to minimizing, and a file folded into the bundle loses them. The
browser was never asked, so 'system' would have meant 'always dark'. That mode
now opts out of the bundle; 'light' and 'dark' carry no attribute and still
share one cached file.

Saving the chosen mode to a member's profile was fatal. Db::insert() wants a
list of rows and was handed a single flat one, which is a critical error rather
than a warning outside backward compatibility mode, so the first time a logged
in member chose a mode the page died. loadVariant() has the identical defect
three lines further down; it is left alone here because this branch neither
enables nor exercises variants, and shipping an unverified fix is worse than
recording a known one.

dark.css itself is written against release-3.0's own tokens rather than ported.
The snapshot on #7933 could not be used: of its 197 overrides only 84 name a
token that exists here, while 213 colour-bearing tokens here have no value in
it. It overrides 243 tokens and restates no rules, all written against twelve
values declared at the top of the file, so the way to review it is to agree
those and then check each component reached for the right one.

The ramps are deliberately untouched, so a variant still re-tints the forum by
moving --primary-color-hue and dark mode moves with it.

Verified on a running forum. Light mode is unchanged: the computed colours of
4254 elements over twelve pages are identical with the file present and absent.
All three modes emit the right markup and the right stylesheets, including the
media attribute on 'system' alone. Eighteen pages were swept in both modes for
text that lands on a background too close to it; dark mode has nineteen elements
below the AA ratio against light mode's hundred and thirty-six, and both dark
causes sit outside the theme - the bundled coloris colour picker, which carries
no tokens at all, and the Administrator group's online_color, which is data. No
errors were logged.

Signed-off-by: Mathias Alberts <mathiaspapealbert@hotmail.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
@albertlast

Copy link
Copy Markdown
Collaborator

Wave 5 is merged, and wave 6 — the profile — is up

All four of wave 5 landed: #9496, #9497, #9498 and #9500. Wave 6 is three
pull requests, all open.

1 #9547 The profile gets its own stylesheet
2 #9548 The last colours the colour mode could not reach
#9549 Two more labels pointing at fields that are not there

Part 1 — and why a block move would have been wrong

index.css carried a "profile section" of about three hundred lines that every page in the
forum downloaded. Lifting it wholesale into a profile.css is the obvious move and it is a
trap, for reasons worth writing down because they are not visible from the section heading:

  • the post list inside it is the same one the moderation centre, personal messages, recent
    posts and search all render;
  • the board picker is shared with search;
  • the paid subscription, theme picker and theme settings blocks belong to the admin centre;
  • #creator is also emitted by the password reminder;
  • and the section opens with a bare dl { overflow: auto; } — a global rule that happens to
    sit under the heading.

A profile-only stylesheet containing those would have quietly unstyled all of them, on pages
nobody would have thought to check.

So the split was done by resolving every selector against the templates that actually emit it,
matching class="…" and id="…" attributes rather than bare words — a plain grep for bar,
post or time matches half the theme — and moving a rule only when the leftmost element of
every one of its branches
is something no template but the profile emits, since anything to
the right of that is already scoped by it. Thirty-five rules moved; thirty stayed.

Two went out entirely rather than moving: .sizefix and .topic .mod_icons name classes that
nothing in Themes/ or Sources/ emits at all.

The snapshot's own profile.css could not be used for this. It is loaded only from the
profile action, yet claims #paid_subscription, which the admin paid-subscriptions template
emits, and #creator, which the reminder page emits — so porting it would have shipped exactly
the regression described above.

Part 2 — two blind spots the last wave left

Wave 5 claimed the theme's colours were all reachable by the colour mode. Two kinds were not,
and neither was visible to the sweep that made the claim.

Borders written as a shorthand. That sweep matched border and border-*-color, so a
border-bottom: 1px solid #bbb went straight past it. Seventeen were left — the hairline under
the header, the rule above a post list, the draft panel's edge, the dotted rule between
attachment previews, the drop zone's hover outline, the mobile notification list, the
right-to-left button row and week grid.

A token that was never declared. This one is subtler and is the more useful lesson.
atwho.css writes every one of its nineteen custom properties with a fallback
var(--mentions-bg, #fff) — so the mentions list has never looked wrong and nothing flagged
it. But a token that is never declared cannot be overridden either, which is precisely what
kept that list white in dark mode. Declaring each one from the fallback it is written with is
the whole fix; atwho.css itself is untouched, so its fallbacks stay the safety net they were
written to be.

Excluding variables.css and dark.css, where literals belong, and the standalone installer,
maintenance and report pages and the editor's iframe stylesheet, which no colour mode reaches,
the theme's stylesheets now hold exactly one literal colour between them — the diagonal
stripe over a progress bar's fill, which is a texture rather than a colour.

Verification

Both parts are strict no-ops in light mode, checked against a running forum rather than by
reading:

pages elements differences
Part 1 22 7260 0
Part 2 14 4473 0

Part 1's twenty-two are all eleven profile areas, the five pages that share those selectors,
the two admin pages that own the blocks left behind, the reminder, the memberlist, the stats
and the board index — chosen so that a wrongly moved rule would have to show up. Part 2 also
records colours resolving differently in dark mode afterwards, which is the point of it.

The profile itself then swept clean: eighteen areas in dark mode, 1854 elements carrying text,
none below a 3.0 contrast ratio against the background actually behind it, nothing
unreachable, and no errors logged.

One measurement worth passing on. Part 1's first comparison reported a single difference — a
stats-page bar reading 254.232px against 0px. It was an artifact: both captures had the
same inline width: 100%, sampling twice on one branch gives the same number both times, the
two rules that moved are scoped under a profile-only ancestor, and the stats page does not load
profile.css at all. A computed-style diff is sensitive to page state and to layout timing,
not only to CSS, so a lone difference is worth chasing to a cause before believing it.

Why wave 6 stops at three

The rest of the profile's delta is not portable, and each piece has a specific reason:

  • All six Sources/Actions/Profile/* diffs are reverts. The snapshot drops the guard that
    stops an empty buddy or ignore list erroring, uses ?? where ?: is correct because the
    language lookup returns an empty string rather than null, carries a file: 'THemes' typo,
    and renames posts_count to count_posts — which is the 2.1 column name. The 3.0 schema
    and every other query in the tree say posts_count.
  • What is left of Profile.template.php restructures the user-menu popup, into classes
    this branch has no CSS for. That is chrome rather than profile, and it belongs with the rest
    of the popup work.
  • profile.js is a timezone-detection rewrite that also deletes the auto-complete
    suppression and the signature preview handler.

Wave 7

Unchanged from the last update, and now next: the admin centre settings grid — where a
CSS-only restyle of the existing dl.settings looks better than renaming it across the twenty
templates that use it; login, registration and reminders, whose templates and stylesheet slice
have to land together; the attachments.css and postbit.css slices, still waiting on #9459,
since every attachment upload currently fails; and tokens for the editor's iframe, the one
place a colour mode still cannot reach.

@albertlast

Copy link
Copy Markdown
Collaborator

Wave 6 is merged; wave 7 is up, and three of its four items closed without needing a pull request

Wave 6 landed in full — #9547, #9548, #9549. Wave 7 is two pull requests, both
open:

#9557 The colour mode reaches the editor itself
#9558 Nine captions bound to the fields they belong to

The colour mode is now everywhere

#9557 closes the last gap. The surface you type on is an iframe with its own document, so
nothing the page declares reaches it, and it stayed white text-on-white in the middle of a
dark forum.

The useful discovery is that the mechanism was already there. The editor plugin already
copies the theme's tokens into that document. It matched selectorText == ':root', and the
colour mode blocks are :root[data-mode="dark"] and friends, so it walked straight past them;
and nothing set data-mode inside the iframe, so even copied they would have selected
nothing. Two small changes to code that was already doing most of the work.

One decision in there is worth stating plainly, because it looks like an omission otherwise.
system is resolved to the mode it currently means before being written into the iframe,
rather than passed through.
Outside, that mode works by asking the browser through a media
attribute on the stylesheet, and no such attribute exists on anything inside. Left as
system, the copied rules would match the reader's own setting — so the editor would go
dark on a forum deliberately kept light. That case was tested directly: browser asking for
dark, forum set to light, editing surface correctly stays white.

Sixteen literals in the editor's own stylesheet became tokens along the way. They are named
for the editing surface rather than pointed at the tokens a rendered post uses, because the
two are different contexts — what you type into has no post background behind it — even where
the values coincide today.

Captions that were never linked

#9558 comes from asking, of every form control in the theme, whether anything gives it
an accessible name: a label, a wrapping label, aria-label, aria-labelledby, title or
placeholder.

pages loaded:            43
controls checked:       484
caption present but unlinked:  26

Nine of those twenty-six are cases where the caption belongs to exactly one control and to
nothing else, and those are what this fixes. No new wording: every caption is already drawn on
the page, just never associated, so a screen reader announces the field with no name and
clicking the caption does nothing.

What makes it reviewable is that the theme already does this correctly elsewhere. The very
same subject caption is a label bound to its input in the posting form and the newsletter
form, and a bare <span> in the personal message and mailing forms. The group form binds the
"required posts" caption on the page that edits a group and leaves it unbound on the page that
adds one. These read as oversights rather than decisions, and they are fixed the way the
working ones already do it.

Left alone deliberately, each because it needs new wording and so deserves its own change: the
ban form, whose captions are already labels and correctly name the checkbox that arms each
trigger rather than the text field beside it; three places where one caption heads several
controls, which want a fieldset and legend; and the 153 controls that have no visible caption
at all, the header search box among them.

Three items closed by measuring rather than building

Wave 7 was planned as four things. Three of them turned out not to need a pull request, and
the evidence matters more than the conclusion.

Attachments need nothing. Now that #9459 has merged and uploads work again, the area was
retested end to end rather than reasoned about: a text file uploads cleanly and gets a plain
insert button with no width and height fields; an image gets the panel toggle that reveals
them. The selector I had previously flagged as broken does match nothing, but it is harmless —
the stylesheet hides that block by default and the image branch builds its own toggle.
attachments.css is also already loaded only by the topic display and the posting form, which
is the right arrangement already.

A postbit.css is not worth extracting, even though the equivalent worked well for the
profile in wave 6. The difference is who draws the markup: post markup comes from sixteen
templates, nine of them for the wrapper alone, across the topic display, personal messages,
search, recent posts, the moderation centre, the profile and posting. A file loaded only where
it is needed would have to load nearly everywhere, so it saves nothing and risks a lot. The
profile was worth doing precisely because one action owns it.

And a correction to something I wrote in the last update. I recorded the admin settings
grid as risky because dl.settings is float-based and floats tolerate mismatched term and
definition counts in a way a grid does not. Measured, that risk does not exist: across
thirty-five settings lists on twenty pages there are no mismatched counts, none nested,
and none using the full-width term. The conversion would be mechanically safe.

What is missing is a reason. The floats already pair each row correctly and clear properly, so
converting them changes how the code is written without changing what anybody sees. That makes
it a redesign rather than a fix, and the right person to decide whether the admin settings
pages should be laid out differently is not me. If it is wanted, it can be done on the existing
class in CSS alone — no rename across the twenty-odd templates that use it, and nothing broken
for mods that target it.

Where that leaves this pull request

The colour mode now reaches every part of the theme it can reach. Excluding the token files
themselves, and the standalone installer, maintenance and report pages that no colour mode
touches, the theme's stylesheets hold one literal colour between them — a translucent
white stripe used as a texture on progress bars.

What is left of this pull request is the two areas that need designing rather than porting:
login, registration and reminders, whose forms move onto a grid class this branch does not
define, so templates and stylesheet must land together and the snapshot's own login template
needs rewriting rather than porting; and the admin settings layout described above. Both are
decisions before they are diffs, which is a different kind of work from the seven waves so far
and probably wants agreeing before anybody starts.

The end condition is unchanged from wave 4: this closes when every remaining hunk is either
shipped or recorded as rejected with its reason, not when the diff is empty.

@albertlast

Copy link
Copy Markdown
Collaborator

Where the split stands, and what is actually left

Waves 1 to 6 are merged. Wave 7 is open as #9557 and #9558.

The reference snapshot this series ports from had drifted a long way behind release-3.0, which
made the remaining-work figure meaningless, so I merged release-3.0 forward into it and
remeasured. Conflicting hunks resolved in favour of the snapshot; non-conflicting upstream work
was absorbed.

213 files → 98. Eight of those are the FontAwesome webfont binaries already dropped with
evidence (nothing references them), so 90 files genuinely differ.

That number still is not a to-do list, and it is worth repeating why, because it looks like a
stall otherwise. The done-condition originally written for this series — "close #7933 when the
diff is empty" — cannot be met. Several waves deliberately superseded the snapshot rather
than copying it. admin.css is the clearest case: release-3.0 now carries 30 var(--admin-*)
tokens, the snapshot carries 1. Those two files will never converge, because the merged version
is the better one. The same is true of dark.css, variables.css and much of index.css.
A large part of the 90 is permanent divergence.


A porting hazard that now matters more than it did

The snapshot predates several merged fixes, and it rewrites whole files. Two confirmed cases,
both landing precisely on the work that remains:

  • its Login.template.php lacks the ajax-submit fix
  • its Register.template.php lacks the reg_username / reg_email label associations

Checking either out wholesale would silently revert a merged fix. Every remaining file the
snapshot rewrites has to be merged by hand, not copied. This is a property of the snapshot's
age, not of the merge I just did — but from here it applies to nearly everything left.


Next step, ready to start, no decisions needed

Finish the RTL migration. rtl.css is still 414 lines across 97 rule blocks; the snapshot
gets it to 19 lines and deletes calendar.rtl.css. The declaration census shows why that is
reachable:

property count
float 34
text-align 17
margin-left / -right 15
clear 8
padding-left / -right 7

81 of about 129 declarations are pure direction flips that float: inline-end,
margin-inline-* and text-align: start/end delete outright. Only a handful cannot go: two
content: icon flips, two background-position, one direction, one rotate.

What makes this the right next wave rather than merely a large one is that the pattern is
already merged and accepted
#9356 and #9357 did exactly this for the float/align utilities
and for dl.settings. The review criterion therefore already exists and does not need arguing,
and the whole thing is verifiable by installing an RTL language and comparing, with no aesthetic
judgement anywhere. It also sweeps up a loose end: the dead .topic .mod_icons rule at
rtl.css:457, whose index.css counterpart was removed in #9547.

After that, large but structured

The editor overhaul, deferred since wave 1 for being too big to review as one unit and still
untouched — about 9 files, +794/−925. Much of the shrink is code moved into new plugins, so it
splits along those seams: quote-fast, xml-preview, drafts, then the remaining modernisation.


Questions, with a recommendation for each

Three of these I can answer myself and have; three need somebody else's call.

1. Do the seven --header-* tokens still describe anything after the layout revert?
Asked here on 2026-08-09. Answered by measurement — no action needed. The assumption behind
the question was that #9427 removed them. It did not: all seven are declared and all seven are
referenced by #header at index.css:2029-2036. Five hold inert values today
(transparent, 0, 0, none, solid at zero width), but that is what a token is for — it
is the hook a variant overrides. Recommend leaving them alone. Consider this closed.

2. Is postbit.css worth extracting, as the profile stylesheet was?
Answered by measurement — no. Post markup is drawn by 16 templates across Display,
PersonalMessage, Search, Recent and ModerationCenter. The profile was worth extracting precisely
because one action owns it; nothing here does.

3. Does the dl.settings mismatch risk block an admin grid?
Answered by measurement — no, and this corrects an earlier note of mine. 35 lists over 20
pages: 0 mismatched dt/dd counts, 0 nested, 0 full-width dt.windowbg. The conversion would
be mechanically safe.

4. Is a visual change to the admin settings layout actually wanted? (needs a decision)
This is the one thing standing between question 3 and a diff. Floats already pair each row, so
the current layout is not broken — this is taste, not repair. Recommendation if the answer is
yes:
a CSS-only restyle of the existing dl.settings, not the snapshot's class rename across
20+ templates. Same result, a fraction of the surface. If the answer is no, I will drop it and
say so, and that part of the diff becomes permanent divergence.

5. Should Login / Register / Reminder be restyled at all? (needs a decision)
The snapshot's versions cannot be ported: Reminder.template.php leaves a <div> unclosed and
has an orphan <h3>; Login.template.php instantiates smf_Login when the class is
SMF_Login, passes sCors and bIsFromAjax unquoted, and drops the error handler entirely.
Its login.js also omits the X-Requested-With header that Login2::checkAjax() needs. So this
is a rewrite on release-3.0's own markup, not a port — a much bigger commitment than the other
areas, and only worth making if the restyle is wanted for its own sake.
Recommendation: leave it until everything cheaper is done.

6. FontAwesome: vendor a release, or drop the icon work? (needs a decision)
icons.css is +702/−13 and is the last large self-contained feature in the diff, but it depends
on icon fonts, and fontawesome_source=local is broken on release-3.0 today — the bundled CSS
has no @font-face at all. Note also that the snapshot's own webfont binaries are 6.3.0 while
its CSS is 7.1.0, so those must not be reused. Vendoring a font release is a maintainer
decision, not a split slice. Recommendation: fix fontawesome_source=local as its own issue
first, separately from this PR, and treat the icon work as gated on that.


One question about pace rather than content

24 PRs from this series are open awaiting review. Wave 4 stopped producing PRs at roughly 90
open for exactly this reason. Both the RTL wave and the editor wave are ready to start, but if
the queue rather than the authoring is the constraint, draining it is worth more than adding to
it. Happy either way — it only needs saying which.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants