Take Basecoat's boxes and states: a table header copied 13 times, 47 empty states in 5 shapes, a pill written 7 ways #291
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Postulo/postulo#291
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The companion to #290. That one takes Basecoat's form family, which arrives without a rename because Postulo owns
field-inputand notfield. This one is the harder half — the boxes and the states — and it is where the duplication actually is.The evidence, counted over the 184 templates
A table header is copied thirteen times. The string
appears 13 times across 12 files, and
<table class="w-full text-start text-sm">12 times. Sixteen templates contain a<table>; two use<c-table.head>. Worse, the abstraction exists twice —cotton/table/head.htmlandallauth/elements/thead.htmleach hold a byte-identical copy of that string — and it is still written out eleven more times. Of 84<th>elements, 39 carryclass="px-4 py-3 font-medium", and the row rule is drawn two incompatible ways:<tr class="border-b …">five times against<tbody class="divide-y …">eight times."There is nothing here" is written about forty-seven times, in five shapes. The same sentence is centred in one place and left-aligned in another:
card py-12 text-center text-sm text-ink-500 dark:text-ink-400card py-12 text-centercard text-sm text-ink-500 dark:text-ink-400px-4 py-6 text-sm text-ink-500 dark:text-ink-400rounded-lg border border-dashed …Two list pages, the same sentence, different boxes:
accounts/invite_list.html:63centres "No invitations yet." without the padding,applications/tag_list.html:44centres "No tags yet." with it. The three plugin pages disagree among themselves — two use the list shape, the third uses a card.A pill is implemented seven ways, and the class that exists is used once.
app.css:380-410defines.tagand seven colours.tag-greyis used once, inpartials/plugin_tags.html:22; the other six colours are dead code. Meanwhile the literal stringrounded-full bg-ink-100 px-2 py-0.5 text-xsis written inline 9 times, in two ink tones (600five times,700three), withfont-mediumpresent in the partials and absent in every inline copy — which is the only thing separating them from the real class.server/logs.html:99draws a square one with its own three-branch tone map, duplicatingpartials/status_badge.html.documents/cv_list.html:33gets a badge by writingchip py-0 text-xs.cardappears 234 times in 31 distinct class strings, with four bottom margins for one stacked layout:mb-6(62),mb-4(19),mb-8(11), none (26).What Basecoat has, and what collides
card(app.css:455)card— a grid with> header, an action slot,> footeralert+ four variantsalert—> section,> footer,:has(> footer)spacingtag+ 7 coloursbadgechip,chip-new,chip-remove,chip-textapp.jsbadge,input-groupmenu-itemitemtable-cards,scroll-xtable,table-containerempty(26 lines)skeleton(6 lines)kbd,breadcrumb,avatar,progressAll of them are pure CSS with no script, so the objection recorded in
scripts/sync-vendor.mjs— that a Basecoat component driven by script has no path with scripts off — does not reach any of them.tests/test_stylesheet.py::test_no_class_is_defined_on_both_sides_of_the_importrefuses an import whose classesapp.cssalso defines, socardandalerteach need a decision taken in the open. That guard is why this has not happened by drift. The trap is the row it cannot catch:tagandbadgeare two names for one idea, so importingbadgewould pass the guard and leave the application with two ways to draw a pill — worse than either. The decision is per concept, not per name.The order
One component per commit, as #262 established, each with its decision written down first:
empty,skeleton,kbd— nothing to decide and nothing to collide with.emptyreplaces 47 hand-written blocks with one component;skeletonis the loading state the application has never had. These are pure gain, they are small, and they prove the paint pattern.table— with<c-table.head>becoming the only table header, the thirteenth copy deleted, and theallauthduplicate pointed at the same component.item,breadcrumb,avatar,progress— no name collision, butmenu-item, the<title>-only Server settings › trail, the initials tile built incore/templatetags/postulo.pyand the funnel bar are the same ideas under other names, so each replaces rather than joins.badge—tagandchipgo with it, the nine inline copies with them, and the seven-colour palette moves onto badge variants, which is what #285 then spends.card— the big one, 234 sites, and the semantic<header>/<footer>is the point rather than the cost.alert— last, because the page-wide region is wired intoapp.js, htmx and the Escape handler, and that is where getting it wrong is silent.What must not change
The full constraint list is in #292. The ones that bite hardest here:
app.css:944-979puts aButtonTextborder onmenu-item,nav-link,chip-removeand the funnel bar, because a high-contrast theme discards backgrounds. Every replacement needs its line there — a card that says "card" only by its background says nothing under that theme.test_template_lint.pyforbidsoverflow-autoandoverflow-x-autoin templates: a table scrolls in.scroll-x. Iftable-containertakes over, the rule moves rather than lapses, and.scroll-x'srelativeis load-bearing — an.sr-onlydescendant escaping the box scrolled every listing page 144 px at 320.table-cards(app.css:608-638) turns a table into cards below 48rem, measured against real pages rather than guessed. Basecoat'stablehas no equivalent, so it survives whatever else changes.skeletonisanimate-pulse, andapp.css:897-906already reduces every animation to 0.01ms for somebody who asked for less motion. Extendtests/e2e/test_text_spacing.py::test_less_motion_means_no_motionto a page showing one, rather than assuming.Done when
emptyandskeletonare used by every list and every slow page that has neither.allauth/elements/thead.htmluses it rather than copying it.test_stylesheet.py's ownership guard passes with the new imports, andbasecoat.csspaints only classes Basecoat defines.