docs: record CSS template inventory + decide unusedSymbols-before-split ordering
feat(css): add content-source collector + dynamic-class safelist for unusedSymbols report
- scripts/css-content-sources.mjs: buildCorpus() gathers templates/public/src
PHP + first-party js/app JS (vendor excluded), returns {corpus, sources,
totalBytes, safelist, prefixes}
- Mined 22 exact runtime classes + 5 DB/state-derived suffix prefixes from
status-badge.php, SystemController statusClass/logLineClass, and class=<?=?>
ternaries
- docs/css-split-analysis.md notes content-corpus section
- td: #11 collect-content-sources done; feeds #12 report script
feat(css): unusedSymbols report script + just css-report recipe
- scripts/css-unused-report.mjs: per-bundle class/id extraction vs buildCorpus()
corpus + safelists; measures reclaimable bytes via lightningcss transform
unusedSymbols (report-only, no stripping to disk)
- just css-report: rebuild CSS then run the report
- css-content-sources.mjs: add VENDOR_CLASS_PREFIXES (filepond--*, htmx-*)
- RESULT: 216,383B total, ~6.2KB (2.9%) reclaimable; FilePond/HTMX exclusion
corrected inflated 26% (56KB) false-positive down to honest 2.9%
- docs/css-split-analysis.md findings table + TODO 12/13 done
docs(css): record go/no-go decision — split NO-GO, pruning conditional-go
- Decision analysis in docs/css-split-analysis.md
- ~6.2KB (2.9%) reclaimable of 216KB; base.min.css only 484B (2.3%)
- SPLIT NO-GO: base.css already well-used; parked u/w/x/y(/z) as deferred
- PRUNING conditional-go on hand-verifiable dead selers from source, never dist;
re-run just css-report after each edit; keep needs-review + vendor-prefix cls
- td: task 14 done; split stream 41 tasks -> 2 pending / 32 done / 7 deferred
todo: defer CSS pruning stream (10/u/w/x/y/z), context updated
8.6 KiB
CSS split / unusedSymbols — Template → Page-type inventory
Status: analysis (gates define-per-page-type-css-bundle and the unusedSymbols content corpus)
How CSS is loaded today (head.php)
App::render() (app/src/App.php) always includes app/templates/head.php for a
full page render. head.php unconditionally emits:
<link rel="stylesheet" href="/assets/dist/base.min.css">
base.min.css = the whole style.css @import chain:
reset → colors → typography → base → all components/* → utilities. So every
full page ships the entire component+utility set, even pages that barely use it
(the cross-page waste the split targets).
On top of it, head.php renders $extraCss array entries (admin prepends
admin.min.css via $extraCssAdmin). Page-specific .min.css bundles load as
$extraCss.
Fragments / HTMX partials (app/templates/partials/*, app/public/**/fragments)
are included directly, not via App::render(), so they do NOT load head.php.
Page-type map (entry → templates → extra css → bodyClass)
Public (non-admin)
| Page type | Template(s) | extraCss (on top of base) | bodyClass | Notes |
|---|---|---|---|---|
| home | public/home.php |
public.min.css |
home-body |
HomeController |
| tfe | public/tfe.php |
tfe.min.css |
tfe-body |
TfeController |
| repertoire/search | public/repertoire.php, public/search.php |
repertoire.min.css |
search-body |
SearchController |
| content-page (about / licence / charte) | public/about.php, public/licence.php, public/charte.php |
content-page.min.css |
apropos-body |
AboutController, LicenceController, CharteController |
| not-found | public/not-found.php |
not-found.min.css |
page-not-found |
Dispatcher |
| partage (student form / recap) | partage/form-page.php (templates/), partage/recapitulatif.php, partage/retry-email.php, partage/index.php |
form.min.css (+ filepond) |
— | FormBootstrap injects extraCss = [form.min.css]; partage/index sets $filepondBase |
Admin
head.php prepends admin.min.css ($extraCssAdmin) whenever $isAdmin is set.
| Page type | Entry (app/public/admin/*.php) | extra extraCss / extraCssAdmin | bodyClass |
|---|---|---|---|
| login / reset | login.php, password-reset.php, request-reset.php |
— | admin-body |
| index | index.php |
filepond*.css (extraCssAdmin) |
admin-body |
| recapitulatif | recapitulatif.php |
— | admin-body student-body (student mode) |
| add / edit | add.php, edit.php, contenus-edit.php |
form deps | admin-body |
| contenu mgmt | contenus.php, tags.php, acces*.php, cleanup.php, account.php, etc. |
— | admin-body |
| parametres | parametres.php |
system.min.css (extraCssAdmin) |
admin-body |
Dist bundles present (app/public/assets/dist/)
From scripts/build-css.mjs: base.min.css, admin.min.css, form.min.css,
public.min.css, tfe.min.css, repertoire.min.css, content-page.min.css,
not-found.min.css, system.min.css, file-access.min.css, common.min.css.
Shared core (potential overlap between bundles)
base.min.css already contains everything: reset, colors, typography, base,
components/{links,focus,forms,tables,dialog,details,media,buttons,badges,toast,
pagination,header,search,toc}, utilities.
The split must decide which of those move OUT of the global base into page-type bundles; the components used on every page (reset, colors, typography, base, header, footer, search? — see component usage below) stay in a slim base.
Next step
Component-by-component usage audit: which templates actually reference each
components/* class / utility. That drives define-per-page-type-css-bundle
and feeds the unusedSymbols content corpus.
Content corpus (for unusedSymbols report)
scripts/css-content-sources.mjs builds the content corpus scanned against the
CSS symbols:
- Sources: app/templates/.php, app/public/.php, app/src/.php (controllers + icon.php helper), app/public/assets/js/app/.js (first-party).
- Vendor JS excluded (htmx/filepond/pdf): their class names are internal to their own bundled CSS, and including them would only under-report project usage — the safe direction.
- Dynamic-class handling:
buildCorpus()returns a safelist of 22 exact runtime class names (status-ok/warn/err/unknown, log-, input-error, active, disabled, btn--, fhb-, admin-icon-btn--, status-published/pending/badge) plus 5 prefix patterns (status-access--, toc-level-, admin-import-log__item--*, admin-body, student-body) whose suffix is DB/state-derived.
Run: node scripts/css-content-sources.mjs (prints inventory).
The report script (scripts/css-unused-report.mjs, task 12) imports buildCorpus().
unusedSymbols report — findings (diagnostic, task 12/14 go-no-go)
just css-report rebuilds CSS then runs scripts/css-unused-report.mjs:
for each dist/*.min.css it extracts class/id symbols, checks them against the
content corpus + dynamic/vendor safelists, and measures bytes lightningcss would
reclaim (no stripping applied).
Result (216,383 B total): ~6.2 KB (2.9%) reclaimable. Per bundle:
| bundle | orig B | reclaim B | candidate-unused |
|---|---|---|---|
| admin.min.css | 55016 | 3060 | admin-import-results*, n-grid, n-section, param-, admin-toggle, admin-dialog--sheet, admin-maintenance-* |
| base.min.css | 21375 | 484 | btn--success, btn--blue, btn--yellow |
| form.min.css | 41855 | 731 | mode-toggle, licence-generalites, file-preview-list |
| partage-form.min.css | 37229 | 731 | (same) |
| form-base.min.css | 19110 | 751 | (same) |
| public.min.css | 4047 | 228 | card__media--placeholder |
| system.min.css | 7408 | 185 | sys-status-section |
| content-page.min.css | 3683 | 32 | heading-permalink |
Vendor classes excluded (would over-report + reclaim): filepond--, htmx-. They are assembled by vendor JS at runtime, so they never appear as literals.
See TODO task 14 for the go/no-go evaluation of this data.
Decision (task 14): per-page split and CSS pruning
Data: ~6.2 KB (2.9%) reclaimable across 216 KB total. base.min.css itself only 484 B (2.3%) reclaimable. Waste is concentrated in admin/form/partage-form (shared FilePond + form CSS).
Diagnosis: base.css is already well-used; the per-page SPLIT would shave minimal real weight (base.min.css is ~all-referenced). The unused-symbol PRUNING opportunity is small and concentrated in bundles that are already page-type- specific (form/admin).
Split — NO-GO
Splitting base.min.css into per-page bundles was premised on cross-page waste of component+utility CSS. The report shows that premise is weak: nearly all of base.min.css's classes are referenced somewhere, and shaving a redundant handful is a pruning task, not a split. A split adds build/template complexity for ~2% of one bundle. → Do not split. Park tasks u-w-x-y as deferred.
Pruning — CONDITIONAL-GO (narrow, safe subset only)
The only pruning worth doing is hand-verifiable dead selectors, removed from SOURCE css files (never from dist) so the next build drops them:
- base.min.css: btn--success, btn--blue, btn--yellow (3, 484 B)
- public.min.css: card__media--placeholder (228 B)
- system.min.css: sys-status-section (185 B)
- content-page.min.css: heading-permalink (32 B)
- form*.min.css: mode-toggle, mode-toggle--back, licence-generalites, file-preview-list (731 B, duplicated across form-base/form/partage-form)
- admin.min.css: admin-import-results*, n-grid, n-section, param-, admin-toggle, admin-dialog--sheet, admin-maintenance-* (3060 B)
Safety rules:
- Delete from app/public/assets/css/** sources, never dist.
- Rebuild + re-run
just css-reportafter each edit; the class must vanish from the candidate list (not just move to needs-review). - Never remove a
needs-review(dynamic emitter) or vendor-prefix class (filepond--, htmx-). - btn--success/blue/yellow are safe (no live $-built variants); confirm they are not emitted by a helper before deleting.
- Green path:
just build-css+ smoke admin/login/form/public pages after.
Verdict: park the split (NO-GO); optionally prune the ~6.2 KB dead set as a follow-up, lowest-risk slice first (base/public/system/content-page).