diff --git a/content/foundation/members/members.toml b/content/foundation/members/members.toml index 30a1b39eb9..bf56a4a2ef 100644 --- a/content/foundation/members/members.toml +++ b/content/foundation/members/members.toml @@ -22,6 +22,7 @@ logo = "connect2x.png" name = "DINUM" website = "https://www.numerique.gouv.fr/" logo = "dinum.svg" +white_bg = true [[silver]] name = "Discourse" @@ -112,6 +113,7 @@ logo = "cinny.svg" name = "Community Moderation Effort" website = "" logo = "community-moderation-effort.png" +white_bg = true [[ecosystem]] name = "Conduit" @@ -127,6 +129,7 @@ logo = "continuwuity.svg" name = "Draupnir" website = "https://github.com/the-draupnir-project/Draupnir" logo = "draupnir.png" +white_bg = true [[ecosystem]] name = "Elm SDK" @@ -137,6 +140,7 @@ logo = "elm-sdk.svg" name = "FluffyChat" website = "https://fluffychat.im/" logo = "fluffychat.svg" +white_bg = true [[ecosystem]] name = "Fractal" @@ -167,6 +171,7 @@ logo = "polychat.svg" name = "Rory&::LibMatrix" website = "https://cgit.rory.gay/matrix" logo = "rory-libmatrix.png" +white_bg = true [[ecosystem]] name = "Thunderbird" diff --git a/sass/_base.scss b/sass/_base.scss index 95e1354d3d..894a6203af 100644 --- a/sass/_base.scss +++ b/sass/_base.scss @@ -18,6 +18,31 @@ --borders-color: #ddd; --notice-box-color: #ffcb006b; + + // Tokens for the support/membership card redesign (_support.scss). + // Kept distinct from the older --borders-color/--color-text-light pair + // above rather than merged into them, since those are already used + // with their own (slightly different) shades elsewhere. + --color-border: #d2d2d2; + --color-text-muted: #555; + --color-bg-tint: #fafafa; + --card-radius: 16px; + --card-radius-sm: 8px; + + // The top 3 "become a member" tiers are literally named after materials + // (Platinum/Gold/Silver) - used as a top-edge accent on .tier-card and a + // frame/flag around the matching tier of supporter logos (_support.scss). + // Platinum is deliberately lighter/cooler and Silver deliberately + // darker/warmer so the two don't read as the same grey. These plain + // colours are for the (thin, non-text) frame border only; the flag is a + // solid fill with white text, which needs its own darker "-badge" pair + // to clear AA (the plain colours are too light/desaturated for that). + --color-tier-platinum: #4596a3; + --color-tier-platinum-badge: #1c3c41; + --color-tier-gold: #d48f2f; + --color-tier-gold-badge: #4b300d; + --color-tier-silver: #969eaa; + --color-tier-silver-badge: #3c3f44; } * { @@ -152,6 +177,10 @@ pre table { @media (max-width: 767px) { padding-inline: 2rem; + // The announcement banner sits directly above .page-header with no + // gap of its own, so on mobile (where the banner text wraps taller) + // the breadcrumb ends up crammed right underneath it. + padding-top: 1.5rem; } padding-bottom: calc(var(--navbar-height)/2); diff --git a/sass/_breadcrumb.scss b/sass/_breadcrumb.scss new file mode 100644 index 0000000000..5dcfc2f0e6 --- /dev/null +++ b/sass/_breadcrumb.scss @@ -0,0 +1,81 @@ +// Breadcrumb navigation +// Used via macros/breadcrumb.html. Two contexts: +// 1. Inside .page-header (dark background) - text is white/muted-white +// 2. Inside .detail-page (light background) - text is muted grey + +.breadcrumb { + ol { + display: flex; + flex-wrap: wrap; + align-items: center; + list-style: none; + margin: 0; + padding: 0; + gap: 0.25rem; + font-size: 0.85rem; + } + + li { + display: flex; + align-items: center; + + // Chevron separator before every item except the first + & + li::before { + content: "/"; + margin-right: 0.25rem; + opacity: 0.45; + } + } + + a, + span { + text-decoration: none; + line-height: 1; + } + + // Dark context (inside .page-header) + .page-header & { + margin-bottom: 0.75rem; + + a { + color: rgb(255 255 255 / 70%); + + &:hover { + color: #fff; + } + } + + // 45% white only clears ~4.4:1 against the black hero background, + // just under AA's 4.5:1 for text; 60% keeps it safely above. + span { + color: rgb(255 255 255 / 60%); + } + + li + li::before { + color: rgb(255 255 255 / 45%); + } + } + + // Light context (inside .detail-page) + .detail-page & { + margin-bottom: 1.25rem; + + a { + color: var(--color-text-muted); + + &:hover { + color: var(--color-text); + } + } + + // The current-page label is text, not decoration, so it still needs + // to clear WCAG AA (4.5:1); #aaa on white only manages ~2.3:1. + span { + color: var(--color-text-light); + } + + li + li::before { + color: #ccc; + } + } +} diff --git a/sass/_membership.scss b/sass/_membership.scss new file mode 100644 index 0000000000..2fcd4333a1 --- /dev/null +++ b/sass/_membership.scss @@ -0,0 +1,249 @@ +.membership { + max-width: 65rem; + padding-inline: 1rem; + margin-inline: auto; + + h3 { + font-size: 1.25rem; + } + + .row { + display: flex; + flex-direction: row; + gap: 2rem; + margin-block: 2rem 3rem; + + @media (max-width: 767px) { + flex-direction: column; + } + } + + .representatives { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + + h3 { + text-align: center; + } + + ul { + padding-inline: 0; + width: 100%; + } + + li { + list-style: none; + text-align: center; + background-color: #fafafa; + border-radius: 8px; + margin-block: 0.5em; + padding: 0.5em 1rem; + } + } + + .tiers-row-wrap { + position: relative; + } + + .tiers-scroll-hint { + display: none; + position: absolute; + // Set by JS (membership.html) to the real vertical centre of the + // first card; this static fallback is only an approximation + // (roughly half the card row's own height down from its top) for + // when JS hasn't run yet. + top: var(--tiers-scroll-hint-top, calc(1.5em + 7.75rem)); + transform: translateY(-50%); + z-index: 3; + width: 2.25rem; + height: 2.25rem; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0.55); + align-items: center; + justify-content: center; + transition: opacity 0.2s ease; + text-decoration: none; + + img { + width: 1rem; + height: 1rem; + // Base asset points down; point it sideways instead. + filter: invert(1); + } + + // Real links to the first/last card (not JS-only), so the row is + // still reachable end-to-end without JS - see membership.html. + &--left { + left: 0.5rem; + // Sane default before JS runs / without JS: hidden, since a + // fresh page load starts scrolled to the left already. + opacity: 0; + + img { + transform: rotate(90deg); + } + } + + &--right { + right: 0.5rem; + // Sane default: visible, since there's normally more to the + // right to scroll to on a fresh load. + opacity: 1; + + img { + transform: rotate(-90deg); + } + } + + @media (max-width: 767px) { + display: flex; + } + } + + .tiers-row-wrap.has-scroll-left .tiers-scroll-hint--left { + opacity: 1; + } + + .tiers-row-wrap.no-scroll-right .tiers-scroll-hint--right { + opacity: 0; + } + + .tiers-row { + display: grid; + grid-template-columns: repeat(4, 15rem); + gap: 1rem; + // These 4 columns are wider than the viewport on most screens, so + // this scrolls sideways (the "scroll to most-popular" script in + // membership.html relies on scrollLeft); it was previously + // overflow-y, which didn't let that horizontal scroll happen at all. + overflow-x: auto; + scroll-snap-type: x proximity; + padding-block-start: 1.5em; + margin-block-end: 2em; + + .tier-card { + border: 1px solid var(--color-border); + border-top-width: 3px; + border-radius: var(--card-radius); + background-color: #fff; + scroll-snap-align: start; + + display: flex; + flex-direction: column; + gap: 0.6rem; + padding: 1.1em; + // The body-wide line-height (1.75) is meant for prose, not a + // dense list of short feature labels in a 15rem-wide card - + // it made wrapped lines look loosely, unevenly spaced. + line-height: 1.4; + + // These tiers are literally named after materials - give + // Silver/Gold/Platinum a top-edge accent in their namesake + // colour (--color-tier-*, _base.scss). Individual isn't a + // material name, so it keeps the plain border. + &--silver { + border-top-color: var(--color-tier-silver); + } + + &--gold { + border-top-color: var(--color-tier-gold); + } + + &--platinum { + border-top-color: var(--color-tier-platinum); + } + + // The individual tier is a different kind of thing than the 3 + // organisation tiers next to it, not just a cheaper option - + // give that group boundary a bit of visual room and a divider, + // centred in the combined grid-gap + margin space between them. + &--group-start { + position: relative; + margin-inline-start: 0.5rem; + + &::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: -0.75rem; + width: 1px; + background-color: var(--color-border); + } + } + + &-eyebrow { + margin: 0; + text-align: center; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--color-text-muted); + } + + h3 { + margin: 0; + text-align: center; + } + + .pricing { + align-self: center; + background-color: #e9e9e9; + border: 1px solid var(--color-border); + border-radius: 9999px; + padding: 0.15rem 0.8em; + font-weight: 700; + } + + ul { + margin: 0; + padding-inline-start: 1.35em; + + li { + list-style-image: url("/assets/tick_supported.svg"); + font-size: 0.9rem; + + & + li { + margin-block-start: 0.4em; + } + } + } + } + + // Sit as extra grid items below the card row (grid-auto-flow keeps + // the default "sparse" packing, so a col-1 item and a col-2/5 item + // land on a new row rather than squeezing into row 1) - inside the + // same scrolling grid as the cards so they track the row's own + // horizontal scroll on mobile instead of drifting out of alignment. + .tiers-row-cta { + display: flex; + + .call-to-action { + width: 100%; + text-align: center; + padding: 0.5em 1em; + // .call-to-action's own font-size (_base.scss) is sized for + // full-width page CTAs and is too large for this 15rem-wide + // column; match the tier-card's own list-item text size + // (ul li, above) instead of picking a new magic number. + font-size: 0.9rem; + } + + &--individual { + grid-column: 1; + } + + &--orgs { + grid-column: 2 / 5; + justify-content: center; + + .call-to-action { + width: auto; + } + } + } + } +} diff --git a/sass/_support.scss b/sass/_support.scss index 5958a4f551..0e31215f56 100644 --- a/sass/_support.scss +++ b/sass/_support.scss @@ -1,161 +1,187 @@ -.support.main { +// Fixed logo box (_support.scss below) + object-fit: contain, instead of +// deriving a width from page-max-width/cards-gap/tier count - simpler to +// read, and doesn't need re-deriving if those values ever change. See +// https://ishadeed.com/article/aligning-logos-css/ for the technique. +.supporters-card { + --supporters-logo-size: 145px; +} - &>div { +// Platinum and Gold render at the same, larger size; only Silver (and +// below) stay at the base size. +#platinum-supporters .supporters-card, +#gold-supporters .supporters-card { + --supporters-logo-size: 190px; +} + +#supporters { + display: flex; + flex-direction: column; + margin-block-end: 5rem; - padding-block: 3rem; + // Full-bleed bands (same idea as .step-background in _try-matrix.scss) + // so each tier reads as its own section rather than all ~40 Silver/ + // Ecosystem/Associate logos blurring into one continuous field. + .supporters-band { + padding-block: 2.5rem; + padding-inline: 1rem; @media (max-width: 767px) { - padding-block: 2rem; + padding-block: 1.5rem; } - &:nth-child(even) { - background-color: #eee; + &:nth-of-type(even) { + background-color: #f0f0f0; } + } - .content { - display: grid; - gap: 1rem; - - h2 { - margin-block: 0; - } - - p:last-child { - margin-block-end: 0; - } - - .call-to-action { - justify-self: left; - } + // Platinum/Gold/Silver side by side instead of stacked. Silver has far + // more members than Platinum/Gold (double digits vs. a couple), so it + // gets more room to flow into (enough for several logos per row instead + // of collapsing to a cramped single column). All 5 tiers sit in one + // flex-wrap row, in DOM order - true masonry (CSS grid-template-rows: + // masonry) is Firefox-only today, and CSS multi-column packs tighter but + // reorders content column-by-column instead of preserving DOM order, so + // neither is usable as the baseline. This plain flex-wrap row is what + // renders with no JS (and what a screen reader/no-JS browser sees): + // Platinum, Gold, Silver, Ecosystem, Associate always in that order, at + // every width - the only downside is that flex rows don't backfill gaps + // the way real masonry would, so a short tile next to a tall one can + // leave whitespace beside it. members-masonry.js progressively + // enhances this into hand-packed columns (see .js-masonry below) once + // it's had a chance to measure real rendered heights; anyone without JS, + // or before it's finished, still gets this fully working layout. + .supporters-tier-flow { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 1.5rem; + max-width: var(--page-with-toc-max-width); + margin-inline: auto; + + .supporters-section { + max-width: none; + margin-inline: 0; } - } -} - -.membership { - - max-width: 65rem; - padding-inline: 1rem; - margin-inline: auto; + // Rough size classes so tiles with more members get more room to + // spread their logos across, without needing an exact width per tier. + // Only meaningful in the flex-wrap (no-JS) row above; .js-masonry + // overrides these back to "fill the column" once JS has taken over. + .supporters-section--sm { + flex: 1 1 16rem; + } - h3 { - font-size: 1.25rem; + .supporters-section--md { + flex: 1 1 22rem; } - .row { - display: flex; - flex-direction: row; - gap: 2rem; - margin-block: 2rem 3rem; + .supporters-section--lg { + flex: 3 1 32rem; + } - @media (max-width: 767px) { + // members-masonry.js's progressive enhancement: it measures each + // tile's natural height (from the flex-wrap row above) and greedily + // places each one, in DOM order, into whichever of these columns is + // currently shortest - the same packing algorithm libraries like + // Masonry.js use. Tiles no longer read strictly left-to-right/top-to- + // bottom, but every column still fills in DOM order, and the underlying + // DOM order (what assistive tech and view-source see) never changes - + // this only moves nodes visually. + .supporters-tier-flow.js-masonry { + flex-wrap: nowrap; + align-items: stretch; + + .supporters-tier-column { + display: flex; flex-direction: column; + flex: 1 1 0; + min-width: 0; + gap: 1.5rem; + } + + // Every tile fills its column - the flex-basis weighting above only + // applies to the un-enhanced single-row layout. + .supporters-section { + flex: none; + width: 100%; } } - .representatives { + .supporters-section { display: flex; flex-direction: column; - align-items: center; - flex: 1; - - h3 { - text-align: center; + max-width: var(--page-max-width); + margin-inline: auto; + width: 100%; + + // Platinum/Gold/Silver get a frame in their namesake colour, sized + // to their own content (not the full band width) with a solid-fill + // flag sitting flush in the top-left corner - a bigger version of + // the same "this is a material tier" hint as .tier-card's top edge, + // without needing to colour every individual logo card inside it. + // Ecosystem/Associate get the same frame+flag shape for visual + // consistency, just in a neutral grey since they're not material + // names and shouldn't imply one. + &--platinum, + &--gold, + &--silver, + &--neutral { + position: relative; + border: 2px solid var(--tier-color); + border-radius: var(--card-radius); + padding: 2.75rem 1.5rem 1.5rem; } - ul { - padding-inline: 0; - width: 100%; + &--platinum { + --tier-color: var(--color-tier-platinum); + --tier-badge-color: var(--color-tier-platinum-badge); } - li { - list-style: none; - text-align: center; - background-color: #fafafa; - border-radius: 8px; - margin-block: .5em; - padding: .5em 1rem; + &--gold { + --tier-color: var(--color-tier-gold); + --tier-badge-color: var(--color-tier-gold-badge); } - } - - .tiers-row { - display: grid; - grid-template-columns: repeat(4, 15rem); - gap: 1rem; - overflow-y: auto; - padding-block-start: 1.5em; - margin-block-end: 2em; - - .tier-card { - border: 1px solid var(--borders-color); - border-radius: 16px; - background-color: #fff; - - display: flex; - flex-direction: column; - padding: 1em; - - h3 { - margin-inline: auto; - margin-block-start: .5rem; - } - - .pricing { - background-color: #e9e9e9; - border-radius: 9999px; - margin-inline: auto; - padding-inline: .8em; - } - ul { - margin-block-start: 1rem; - padding-inline-start: 1em; - - li { - list-style-image: url('/assets/tick_supported.svg'); - margin-block: .3em; - } - } + &--silver { + --tier-color: var(--color-tier-silver); + --tier-badge-color: var(--color-tier-silver-badge); } - } -} - -#platinum-supporters .supporters-card { - --cards-per-row: 4; -} - -#gold-supporters .supporters-card { - --cards-per-row: 4; -} - -#silver-supporters .supporters-card { - --cards-per-row: 5; -} - -#associate-supporters .supporters-card { - --cards-per-row: 5; -} - -#ecosystem-supporters .supporters-card { - --cards-per-row: 5; -} -#supporters { - display: flex; - flex-direction: column; - max-width: var(--page-max-width); - margin-inline: auto; - margin-block-end: 5rem; - --supporters-padding-inline: 1rem; - padding-inline: var(--supporters-padding-inline); + &--neutral { + --tier-color: var(--color-border); + --tier-badge-color: var(--color-text-muted); + } - .supporters-section { - display: flex; - flex-direction: column; + // Separate from .supporters-section-flag (the decorative corner + // flourish, which scrolls away with the section) - this stays + // pinned near the top of the viewport while a tall section scrolls + // past, so it's still clear which tier's logos are on screen. + .supporters-section-sticky-label { + align-self: flex-start; + position: sticky; + top: calc(var(--navbar-height) + 0.5rem); + z-index: 2; + margin-bottom: 0.75rem; + padding: 0.2rem 0.7rem; + border-radius: 9999px; + background-color: var(--tier-badge-color); + color: #fff; + font-size: 0.75rem; + font-weight: 700; + + // Hidden until JS (membership.html) marks the section as + // .is-stuck, i.e. the corner flag has scrolled out of view - + // otherwise this sits right next to that flag and just reads + // as a duplicate label. + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + } - h2 { - text-align: center; + &.is-stuck .supporters-section-sticky-label { + opacity: 1; + pointer-events: auto; } .cards { @@ -166,14 +192,18 @@ gap: var(--cards-gap); justify-content: center; + // Matches .account-tier (_try-matrix.scss): a plain border, no + // background tint of its own - it just sits on whatever the + // band behind it already is. A separate card tint on top of the + // alternating band tint was two different greys stacked, which + // read as odd rather than as a clean "cell". .supporters-card { display: flex; flex-direction: column; justify-content: center; align-items: center; - --supporters-card-border-width: 1px; - border: var(--supporters-card-border-width) solid var(--borders-color); - border-radius: 16px; + border: 1px solid var(--color-border); + border-radius: var(--card-radius-sm); --supporters-card-hpadding: 8px; padding: var(--supporters-card-hpadding); // The whitespace of the text at the bottom visually adds to the padding so we account for this on the top a little. @@ -181,12 +211,78 @@ // This adds some space between the text and the image gap: var(--supporters-card-hpadding); + // These are links to the member's site, but had no colour + // override or hover/focus feedback, so they rendered as a + // plain underlined blue link name sitting under the logo. + color: inherit; + text-decoration: none; + outline: 2px solid transparent; + outline-offset: 2px; + transition: border-color 0.15s ease; + + &:hover { + border-color: #888; + } + + &:focus-visible { + outline-color: #000; + } + + span { + font-size: 0.85rem; + // The body-wide line-height (1.75) left wrapped 2-line + // company names looking loosely spaced in these small + // cards; overflow-wrap guards against long single-word + // names overflowing the fixed-width card. + line-height: 1.3; + text-align: center; + overflow-wrap: break-word; + } + + // Some source logo files have a lot of whitespace baked in + // around the mark, others are cropped tight - object-fit + // can't tell the difference, so those tight ones end up + // looking much bigger than the padded ones at the same box + // size. This inset padding doesn't fully equalise that (only + // re-cropping the source files would), but it shrinks every + // logo by the same amount, which shrinks the gap between + // them in absolute terms too. img { - aspect-ratio: 1/1; + box-sizing: border-box; + width: var(--supporters-logo-size, 145px); + height: var(--supporters-logo-size, 145px); + padding: 10px; object-fit: contain; - width: calc((var(--page-max-width) - 2* var(--supporters-padding-inline) - calc(var(--cards-per-row) - 1)* var(--cards-gap)) / var(--cards-per-row) - 2* var(--supporters-card-hpadding) - 2* var(--supporters-card-border-width)); + } + + // A few member logos ship with a baked-in white background + // (flagged via `white_bg = true` in members.toml), which + // shows as a visible white box against the card's tint. + // multiply drops the white and keeps the logo's own colours. + .white-bg-logo { + mix-blend-mode: multiply; } } } } + + // --tier-badge-color is set on .supporters-section--* above and + // inherited down to this span through the DOM, regardless of where this + // rule sits in the stylesheet. Sits flush in the frame's corner (not + // straddling the border like a badge-on-a-line) - its own top-left + // radius matches the frame's, so the two corners read as one shape. + .supporters-section-flag { + position: absolute; + // Offset by the frame's own border-width so the flag's outer corner + // aligns with the frame's outer corner exactly, instead of sitting + // one border-width inset from it. + top: -2px; + left: -2px; + padding: 0.4rem 1rem; + background-color: var(--tier-badge-color); + color: #fff; + font-size: 0.8rem; + font-weight: 700; + border-radius: var(--card-radius) 0 0.6rem 0; + } } diff --git a/sass/_try-matrix.scss b/sass/_try-matrix.scss index cc05e6741c..14acc7c443 100644 --- a/sass/_try-matrix.scss +++ b/sass/_try-matrix.scss @@ -12,7 +12,11 @@ } } -.try_matrix { +// Generic "sequence of full-width steps" layout: alternating-background +// blocks, each a centred column of instructions + a CTA row. Originally +// built for try-matrix.html; reused by support.html and anywhere else that +// wants the same step-by-step feel instead of a card grid. +.step-flow { display: flex; flex-direction: column; @@ -21,6 +25,12 @@ @media (max-width: 767px) { padding: 1rem; + // The sticky header's real rendered height on mobile is taller + // than the site-wide scroll-padding-top (_base.scss) accounts + // for, so an anchor jump lands with the heading tucked behind + // it. Desktop's header is shorter and the site-wide value + // already covers it fine, so this stays mobile-only. + scroll-margin-top: 4.625rem; } &.coloured { diff --git a/sass/style.scss b/sass/style.scss index 93e0813b06..1907898c0f 100644 --- a/sass/style.scss +++ b/sass/style.scss @@ -1,6 +1,7 @@ @import '_normalize'; @import '_fonts'; @import '_base'; +@import '_breadcrumb'; @import '_tables'; @import '_footer'; @import '_header'; @@ -21,6 +22,7 @@ @import '_security-hall-of-fame'; @import '_hosting'; @import '_try-matrix'; +@import '_membership'; @import '_support'; @import '_howitworks'; @import '_elections'; diff --git a/static/js/members-masonry.js b/static/js/members-masonry.js new file mode 100644 index 0000000000..61f19239ec --- /dev/null +++ b/static/js/members-masonry.js @@ -0,0 +1,94 @@ +// Progressive enhancement for the Platinum/Gold/Silver/Ecosystem/Associate +// tier tiles (#supporters .supporters-tier-flow in membership.html). +// +// Without this script, the tiles are a plain flex-wrap row (see +// .supporters-tier-flow in _support.scss) - it keeps them in DOM order +// (Platinum, Gold, Silver, Ecosystem, Associate) at every width, which is +// what a screen reader or a no-JS browser sees, but it can leave whitespace +// next to a short tile in the same row as a tall one, since flex rows don't +// backfill gaps from a later row. +// +// True masonry (grid-template-rows: masonry) is Firefox-only today, and CSS +// multi-column reorders content column-by-column instead of preserving +// reading order - neither is usable here. This does the packing by hand: +// measure each tile's natural height, then greedily place each one (in DOM +// order) into whichever column is currently shortest. That's the same +// algorithm libraries like Masonry.js use. Reading order is no longer +// strictly left-to-right top-to-bottom, but every tile still appears before +// the next columns starts filling behind it, and DOM order (and so the +// no-JS/reader order) is untouched - this only moves nodes visually. +(() => { + const flow = document.querySelector(".supporters-tier-flow"); + if (!flow) { + return; + } + + function columnCountFor(width) { + if (width < 700) { + return 1; + } + if (width < 1100) { + return 2; + } + return 3; + } + + // Captured once, before this script ever touches the DOM, so it always + // reflects the true Platinum/Gold/Silver/Ecosystem/Associate order - + // not document order, which stops matching that after the first layout + // pass moves tiles into columns (a resize re-running this would + // otherwise re-derive the processing order from wherever the previous + // pass happened to leave each tile, compounding drift on every resize). + const originalOrder = Array.from( + flow.querySelectorAll(".supporters-section") + ); + + function layout() { + // Undo any previous columns first, so heights are always measured + // from the flat flex-wrap state, not from inside a narrower column. + const items = originalOrder; + flow.classList.remove("js-masonry"); + for (const item of items) { + flow.appendChild(item); + } + for (const column of flow.querySelectorAll(".supporters-tier-column")) { + column.remove(); + } + + const columnCount = columnCountFor(flow.clientWidth); + if (columnCount <= 1) { + // The plain flex-wrap layout already reads fine as a single + // column; no need to introduce column wrappers for it. + return; + } + + const heights = items.map((item) => item.offsetHeight); + + const columns = Array.from({ length: columnCount }, () => { + const column = document.createElement("div"); + column.className = "supporters-tier-column"; + return column; + }); + const columnHeights = new Array(columnCount).fill(0); + + items.forEach((item, i) => { + const shortest = columnHeights.indexOf(Math.min(...columnHeights)); + columns[shortest].appendChild(item); + columnHeights[shortest] += heights[i]; + }); + + flow.classList.add("js-masonry"); + for (const column of columns) { + flow.appendChild(column); + } + } + + let resizeTimeout; + function scheduleLayout() { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(layout, 150); + } + + document.addEventListener("DOMContentLoaded", layout); + window.addEventListener("resize", scheduleLayout); +})(); diff --git a/templates/macros/breadcrumb.html b/templates/macros/breadcrumb.html new file mode 100644 index 0000000000..506f8b8d79 --- /dev/null +++ b/templates/macros/breadcrumb.html @@ -0,0 +1,18 @@ +{% macro breadcrumb(l1_label, l1_path, l2_label="", l2_path="", l3_label="") %} + +{% endmacro %} diff --git a/templates/membership.html b/templates/membership.html index 361fd68114..c695d3d150 100644 --- a/templates/membership.html +++ b/templates/membership.html @@ -1,20 +1,79 @@ {% extends "page.html" %} {% import "macros/banner.html" as banner %} +{% import "macros/breadcrumb.html" as breadcrumb %} {% block head_extra %} + {% endblock head_extra %} {% block content %} {% set supporters = load_data(path="content/foundation/members/members.toml") %} -