diff --git a/assets/styles/_root.scss b/assets/styles/_root.scss index dbeafd53b6..c51239d9b6 100644 --- a/assets/styles/_root.scss +++ b/assets/styles/_root.scss @@ -154,7 +154,7 @@ --#{$prefix}#{$color}-border-subtle: #{$value}; } - //--#{$prefix}heading-color: #{$headings-color-dark}; + --#{$prefix}heading-color: #{$headings-color-dark}; --#{$prefix}code-color: #{$code-color-dark}; //--#{$prefix}highlight-color: #{$mark-color-dark}; diff --git a/assets/styles/_variables.scss b/assets/styles/_variables.scss index 0a3bc2c897..a7d79985f5 100644 --- a/assets/styles/_variables.scss +++ b/assets/styles/_variables.scss @@ -24,6 +24,10 @@ $background-gray-dark: #131315; $title-size-small: 1.125rem; // Overrides +// Badges: fully pill-shaped and a slightly lighter weight, site-wide. +$badge-border-radius: $border-radius-pill; +$badge-font-weight: 600; + $body-bg-dark: #0E0E11; $body-color: $gewis-gray-dark; $body-color-dark: $gewis-gray-light; @@ -39,6 +43,10 @@ $border-color-translucent: rgba($gray-500, .175); $display-font-family: "Raleway", sans-serif; $headings-margin-bottom: 0; +// Headings are heavier and higher-contrast than body text: full black in light mode, white in gewis-night. +$headings-font-weight: 600; +$headings-color: $black; +$headings-color-dark: $white; $link-color: $gewis-red; $link-decoration: none; diff --git a/assets/styles/components/_activity.scss b/assets/styles/components/_activity.scss index 880b8d72c1..d28ba46253 100644 --- a/assets/styles/components/_activity.scss +++ b/assets/styles/components/_activity.scss @@ -53,3 +53,91 @@ margin-bottom: 0; } } + +// Activity overview list items reuse the shared .panel, but with a calmer, flatter surface than the default panel: the +// body background plus a border, so a long dense list reads as separated rows rather than a stack of filled cards. The +// selector is `.panel.panel-surface` (specificity 0,2,0) on purpose: _activity.scss is imported before _panel.scss, so +// a single-class modifier would lose the source-order tie to `.panel { --bs-panel-bg }`. +.panel.panel-surface { + --#{$prefix}panel-bg: var(--#{$prefix}body-bg); + + transition: background-color $transition-duration ease; + + // The whole panel is clickable (see activity_item_controller.ts); a subtle background lift signals that on hover. + &:hover { + --#{$prefix}panel-bg: var(--#{$prefix}tertiary-bg); + } +} + +@include color-mode(gewis-night) { + .panel.panel-surface { + --#{$prefix}panel-bg: var(--#{$prefix}body-bg); + + &:hover { + --#{$prefix}panel-bg: var(--#{$prefix}secondary-bg); + } + } +} + +// Stacked date on the left of each activity list item: red uppercase month on top, bold body-coloured day, small +// weekday underneath. No background box — it sits directly on the panel surface and is top-aligned with the title. +.activity-date-badge { + display: flex; + flex: 0 0 auto; + flex-direction: column; + align-items: center; + width: 3.5rem; + line-height: 1.2; + text-align: center; + + .activity-date-badge-month { + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + white-space: nowrap; + color: $gewis-red; + } + + .activity-date-badge-day { + font-size: 1.25rem; + font-weight: 700; + white-space: nowrap; + color: var(--#{$prefix}body-color); + } + + .activity-date-badge-weekday { + font-size: 0.75rem; + white-space: nowrap; + color: var(--#{$prefix}secondary-color); + } + + // Multi-day / cross-month ranges carry more text ("31–02", "Jul–Aug"), so shrink the day figure to fit. + &.activity-date-badge-range .activity-date-badge-day { + font-size: 0.95rem; + letter-spacing: -0.02em; + } +} + +// Uppercase month divider ("JULY 2026") with a trailing rule, used in both the upcoming and archive overviews. +.activity-month-divider { + display: flex; + align-items: center; + gap: 0.75rem; + margin: 1.5rem 0 0.75rem; + color: var(--#{$prefix}secondary-color); + font-size: 0.875rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + + &:first-child { + margin-top: 0; + } + + &::after { + content: ""; + flex: 1 1 auto; + border-top: var(--#{$prefix}border-width) solid var(--#{$prefix}border-color); + } +} diff --git a/assets/styles/components/_badge.scss b/assets/styles/components/_badge.scss index 28ae030d16..c5f5aa8048 100644 --- a/assets/styles/components/_badge.scss +++ b/assets/styles/components/_badge.scss @@ -1,27 +1,49 @@ -// Badges are like alerts, they use the same CSS custom properties (`--bs-{state}-bg-subtle`, -// `--bs-{state}-text-emphasis`, `--bs-{state}-border-subtle`). +// Filled, subtle badge variants (one per theme colour). They use the same CSS custom properties as alerts +// (`--bs-{state}-bg-subtle`, `--bs-{state}-text-emphasis`). No border: the (default) pill shape carries the visual +// weight, and a border on a subtle fill just muddies it. @each $state, $value in $theme-colors { .badge-#{$state} { --#{$prefix}badge-color: var(--#{$prefix}#{$state}-text-emphasis); background-color: var(--#{$prefix}#{$state}-bg-subtle); - border: var(--#{$prefix}border-width) var(--#{$prefix}border-style) - var(--#{$prefix}#{$state}-border-subtle); } } -// Neutral badge variant. Built from the surface colours instead of the state colours, so it is subtle in both -// `gewis-day` and `gewis-night`. +// GEWIS-red brand badge, keyed to the brand colour (which is not one of Bootstrap's theme colours). Same subtle, +// borderless treatment as `.badge-{state}`; named `-primary` to match the brand's primary/secondary naming. +.badge-gewis-primary { + --#{$prefix}badge-color: #{shade-color($gewis-red, 20%)}; + + background-color: #{tint-color($gewis-red, 80%)}; +} + +@include color-mode(gewis-night) { + .badge-gewis-primary { + --#{$prefix}badge-color: #{tint-color($gewis-red, 40%)}; + + background-color: rgba($gewis-red, 0.2); + } +} + +// Neutral "tag" variant: transparent fill with a visible border and muted text. Reads as a quiet, secondary chip — use +// it for labels and similar tags. Subtle in both `gewis-day` and `gewis-night` because it is built from surface tokens. .badge-tag { - --#{$prefix}badge-color: var(--#{$prefix}body-color); - background-color: var(--#{$prefix}secondary-bg); - border: var(--#{$prefix}border-width) var(--#{$prefix}border-style) - var(--#{$prefix}border-color); + --#{$prefix}badge-color: var(--#{$prefix}secondary-color); + + background-color: transparent; + border: var(--#{$prefix}border-width) var(--#{$prefix}border-style) var(--#{$prefix}border-color); } -// Outlined: transparent fill, coloured border + text from the paired `.badge-{state}` variant. Use as -// `class="badge badge-secondary badge-outlined"`. +// Outlined: transparent fill with a border and text taken from `currentColor`. Pair with a `.badge-{state}` to colour +// it, e.g. `class="badge badge-secondary badge-outlined"`. .badge-outlined { background-color: transparent; + border: var(--#{$prefix}border-width) var(--#{$prefix}border-style) currentcolor; +} + +// Less-rounded corners: overrides the app-wide pill radius with the standard border-radius for a calmer, boxier badge +// (e.g. the sign-up deadline). +.badge-squared { + --#{$prefix}badge-border-radius: var(--#{$prefix}border-radius); } // Selectable: hidden input + label.badge pattern, mirroring Bootstrap's `btn-check`. diff --git a/src/Twig/Extensions/ActivityDateRangeExtension.php b/src/Twig/Extensions/ActivityDateRangeExtension.php index a531f80b9d..71567529ea 100644 --- a/src/Twig/Extensions/ActivityDateRangeExtension.php +++ b/src/Twig/Extensions/ActivityDateRangeExtension.php @@ -34,6 +34,96 @@ public function getFunctions(): array 'activity_date_range', $this->activityDateRange(...), ), + new TwigFunction( + 'activity_date_badge', + $this->activityDateBadge(...), + ), + ]; + } + + /** + * Locale-aware split date parts for the left-hand stacked badge on the activity overview. Uppercasing is left to + * CSS (text-transform) to stay locale-correct for weekday/month abbreviations. The year is deliberately omitted to + * keep the badge compact; activity_date_range() beside the title already shows the year when it differs from today. + * + * @return array{weekday: string, day: string, month: string, range: bool} + */ + public function activityDateBadge( + DateTimeInterface $begin, + DateTimeInterface $end, + ): array { + $locale = Locale::getDefault(); + $sameDay = $begin->format('Y-m-d') === $end->format('Y-m-d'); + $sameMonth = $begin->format('Y-m') === $end->format('Y-m'); + + if ($sameDay) { + return [ + 'weekday' => $this->format( + $begin, + 'EEE', + $locale, + ), + 'day' => $this->format( + $begin, + 'd', + $locale, + ), + 'month' => $this->format( + $begin, + 'MMM', + $locale, + ), + 'range' => false, + ]; + } + + return [ + 'weekday' => sprintf( + '%s - %s', + $this->format( + $begin, + 'EEE', + $locale, + ), + $this->format( + $end, + 'EEE', + $locale, + ), + ), + 'day' => sprintf( + '%s - %s', + $this->format( + $begin, + 'dd', + $locale, + ), + $this->format( + $end, + 'dd', + $locale, + ), + ), + 'month' => $sameMonth + ? $this->format( + $begin, + 'MMM', + $locale, + ) + : sprintf( + '%s - %s', + $this->format( + $begin, + 'MMM', + $locale, + ), + $this->format( + $end, + 'MMM', + $locale, + ), + ), + 'range' => true, ]; } diff --git a/templates/activity/admin/approvals/review.html.twig b/templates/activity/admin/approvals/review.html.twig index 77c233a5cb..687763c93a 100644 --- a/templates/activity/admin/approvals/review.html.twig +++ b/templates/activity/admin/approvals/review.html.twig @@ -1,8 +1,5 @@ {% extends 'layout-admin.html.twig' %} -{# A localised text (a sign-up list's name) shown the same way as the Details panel: two flag-headed columns - (Dutch | English), each with the inline diff of that language's value. A null `old` shows just the new value (a new - or removed list, which has nothing to compare against); `struck` strikes both values through (a removed list). #} {% macro localised_flag_diff(old, new, renderer, struck) %}
@@ -16,8 +13,6 @@
{% endmacro %} -{# One labelled field of a localised text block: a small label with the inline diff of a single language's value - below it. Renders nothing when the field is empty on both sides, so an unused language collapses to one note. #} {% macro field_diff(label, old, new, renderer) %} {% set rendered = diff(old, new, renderer|default('Combined')) %} {% if rendered|trim is not empty %} @@ -28,9 +23,6 @@ {% endif %} {% endmacro %} -{# A single boolean list setting. With a `previous` counterpart a newly-enabled flag is green, a newly-disabled one - is shown struck-through in red, and an unchanged-but-on flag stays neutral; without one (a new/removed list) an - enabled flag is just shown neutral. An unchanged-off flag renders nothing. #} {% macro flag(label, old, new) %} {% if old is not null and new and not old %} {{ label }} @@ -41,9 +33,6 @@ {% endif %} {% endmacro %} -{# The window, settings and custom fields of one sign-up list. With a lineage-matched `previous` counterpart every - part is shown as a diff (current value, with the old one struck/annotated when it changed, mirroring the main - body above); without one (a new or removed list) it just shows the list's own values. #} {% macro signup_list_body(list, previous) %} {% import _self as diffmacro %}
@@ -67,8 +56,6 @@
{{ flags }}
{% endif %} - {# Custom fields, matched to the previous list's fields by position (mirroring the change detection in the - controller); trailing previous-only fields were removed. #} {% set previousFields = previous is not null ? previous.fields.values : [] %} {% if list.fields|length > 0 or previousFields|length > 0 %}
{% endif %} {{ form_start(decisionForm, {'action': path('admin/activities/approvals/decide', {'revision': revision.getId})}) }} - {# The message field is only present for a decision (reviewer feedback) or a resubmission - (the organiser's required response); a fresh submit / start review / close gets no field. #} + {# The message field is only present for a decision (feedback) or a resubmission (required response); a fresh submit / start review / close has none. #} {% if decisionForm.message is defined %} {{ form_row(decisionForm.message) }} {% endif %} - {# The progressing action is prominent (full-width, solid); the negative actions are smaller - outline buttons inline below it. Each tier self-sizes to whatever the workflow enabled. #}
{% if decisionForm.submit is defined %} {{ form_widget(decisionForm.submit, {'attr': {'class': 'btn btn-success'}}) }} diff --git a/templates/activity/admin/edit.html.twig b/templates/activity/admin/edit.html.twig index b8d5e7e772..417200b294 100644 --- a/templates/activity/admin/edit.html.twig +++ b/templates/activity/admin/edit.html.twig @@ -45,7 +45,6 @@
{% if hasFeedback %} - {# A sidebar so the reviewer's feedback stays visible no matter which step the editor is on. #}
diff --git a/templates/activity/admin/signups.html.twig b/templates/activity/admin/signups.html.twig index 46f4807563..e345fc6100 100644 --- a/templates/activity/admin/signups.html.twig +++ b/templates/activity/admin/signups.html.twig @@ -43,6 +43,5 @@
- {# Shared confirmation modal for the draw, kept outside the live component so its re-render can't orphan it. #} {% include 'partials/application/confirm-modal.html.twig' %} {% endblock %} diff --git a/templates/activity/archive-my.html.twig b/templates/activity/archive-my.html.twig index d67a4a55ff..5fcf551030 100644 --- a/templates/activity/archive-my.html.twig +++ b/templates/activity/archive-my.html.twig @@ -7,6 +7,8 @@

{{ 'My past activities'|trans }}

- {# Shared confirm-modal driving the component's unsubscribe live action; rendered once, OUTSIDE the live component. #} {% include 'partials/application/confirm-modal.html.twig' %} {% endblock %} diff --git a/templates/activity/index.html.twig b/templates/activity/index.html.twig index e49a0cbd08..7907cc5599 100644 --- a/templates/activity/index.html.twig +++ b/templates/activity/index.html.twig @@ -3,7 +3,11 @@ {% block page_title %}{{ 'Activities'|trans|page_title }}{{ parent() }}{% endblock %} {% block contents %} -

{{ 'Activities'|trans }}

+
+

{{ 'Activities'|trans }}

+ + {% include 'partials/activity/filter-toggle.html.twig' %} +
{{ component('Activity:ActivityOverview') }} {% endblock %} diff --git a/templates/activity/my.html.twig b/templates/activity/my.html.twig index 35231b6fdc..65e67ff6bc 100644 --- a/templates/activity/my.html.twig +++ b/templates/activity/my.html.twig @@ -6,9 +6,13 @@

{{ 'My upcoming activities'|trans }}

- - {{ 'My past activities'|trans }} - +
+ {% include 'partials/activity/filter-toggle.html.twig' %} + + + {{ 'My past activities'|trans }} + +
{{ component('Activity:ActivityOverview', { subscribed: true }) }} diff --git a/templates/activity/view.html.twig b/templates/activity/view.html.twig index 7b72cdae64..f1d19c1782 100644 --- a/templates/activity/view.html.twig +++ b/templates/activity/view.html.twig @@ -41,7 +41,7 @@

{{ activity_title(activity) }}

{% if activity.getCategory.value not in ['other', 'uncategorised'] %} - {{ activity.getCategory|trans }} + {{ activity.getCategory|trans }} {% endif %} {% for label in activity.getLabels %} {{ label.getName|localise_text }} @@ -126,7 +126,6 @@ {% if activeLists is not empty %}

{{ 'Sign-up lists'|trans }}

- {# Auto-open the first list that is still open for sign-up; leave the rest collapsed. #} {% set openedFirst = false %} {% for list in activeLists %} {% set autoOpen = not openedFirst and list.isOpen %} @@ -151,8 +150,6 @@ {% endif %} {% if is_granted('ROLE_USER') %} - {# Shared confirm-modal driving each list component's unsubscribe live action; rendered once per page, - OUTSIDE the live components. #} {% include 'partials/application/confirm-modal.html.twig' %} {% endif %} {% endif %} diff --git a/templates/components/Activity/ActivityOverview.html.twig b/templates/components/Activity/ActivityOverview.html.twig index d5e093d9c0..9e571d8ea9 100644 --- a/templates/components/Activity/ActivityOverview.html.twig +++ b/templates/components/Activity/ActivityOverview.html.twig @@ -1,13 +1,7 @@
-
- -
+ {# data-live-ignore: keeps the injected label chips intact across re-renders. #} +
+
@@ -58,10 +52,6 @@
{% if this.labels is not empty %}
- {# Same removable-chip control as the activity form. It only hides and toggles the checkboxes, so - the data-model="labelFilters" binding keeps driving the LiveProp; it sits inside the - data-live-ignore panel above, so a re-render never clobbers the injected chips. The field - label stays outside the controller element so it is not displaced by the injected chips. #}
{% if this.past %} - {% for year, activities in this.activitiesByAssociationYear %} -

{{ year }}

-
+
+ {% for activities in this.activitiesByAssociationYear %} + {% set currentMonth = null %} {% for activity in activities %} + {% set month = activity.getBeginTime|format_datetime(pattern: 'MMMM yyyy', locale: app.request.locale) %} + {% if month != currentMonth %} +
{{ month }}
+ {% set currentMonth = month %} + {% endif %} {% include 'partials/activity/list-item.html.twig' with {'activity': activity} only %} {% endfor %} -
- {% else %} -

{{ 'No activities match the current filters.'|trans }}

- {% endfor %} + {% else %} +

{{ 'No activities match the current filters.'|trans }}

+ {% endfor %} +
{% else %}
+ {% set currentMonth = null %} {% for activity in this.activities %} + {% set month = activity.getBeginTime|format_datetime(pattern: 'MMMM yyyy', locale: app.request.locale) %} + {% if month != currentMonth %} +
{{ month }}
+ {% set currentMonth = month %} + {% endif %} {% include 'partials/activity/list-item.html.twig' with {'activity': activity} only %} {% else %}

{{ 'No activities match the current filters.'|trans }}

diff --git a/templates/components/Activity/Admin/ActivityOverview.html.twig b/templates/components/Activity/Admin/ActivityOverview.html.twig index 4e8f48cf5f..476a00bcf2 100644 --- a/templates/components/Activity/Admin/ActivityOverview.html.twig +++ b/templates/components/Activity/Admin/ActivityOverview.html.twig @@ -105,7 +105,6 @@ {{ 'Submit for review'|trans }} {% elseif row.status.value == 'rejected' %} - {# A closed revision is final (the board ended it), so it offers no reopen. #}
- {# Potentially thousands of rows: collapsed by default and paginated. Collapse is driven by the `expanded` live - prop (not Bootstrap's client-side collapse) so it survives the Ajax re-render that pagination triggers. #} + {# Collapse is driven by the `expanded` live prop, not Bootstrap, so it survives the Ajax re-render pagination triggers. #}
{% if list.isOpen and not list.frozen %} - {# Manual external add is a normal page (it carries the dynamic sign-up form); a redirect back - here refreshes this component. Hidden while frozen: the controller refuses it anyway. #} {{ 'Add external subscriber'|trans }} {% endif %} {% if list.fieldColumns is not empty %} - {# Per-list column chooser: hide custom-field columns that clutter a wide table. Toggling - re-renders the component; the dropdown stays open thanks to data-bs-auto-close="outside" - plus the live component's ExternalMutationTracker preserving Bootstrap's `show` across - the DOM morph. #} + {# data-bs-auto-close="outside" plus ExternalMutationTracker keep this dropdown open across the re-render a toggle triggers. #}
- {# data-live-ignore: the markdown-editor controller injects its own DOM here; keep the - component's re-render/morph from destroying or duplicating it. #} + {# data-live-ignore: the markdown-editor injects its own DOM here; keep the re-render from destroying it. #}
- {# Floating-label textareas need an explicit height (rows is ignored once floating). #} + {# Floating-label textareas need an explicit height (rows is ignored). #}