The prior type of `service` hid that `services?.[0]` is
undefined for embedded bots without stored `config_data`,
forcing a defensive trailing `assert(service && ...)` at the
outgoing render. It also duplicated the union already
exported from `bot_data`.
Co-authored-by: Aditya Kasaudhan <akasaudhan02@gmail.com>
Co-authored-by: Satyam Bansal <sbansal1999@gmail.com>
This matches the naming convention of the existing
OUTGOING_WEBHOOK_BOT_TYPE_INT constant, since both hold integer values.
Co-authored-by: Aditya Kasaudhan <akasaudhan02@gmail.com>
Co-authored-by: Mukul Goyal <96649866+Mukul1235@users.noreply.github.com>
When a user partially selects text inside a rendered <time> element,
Chrome's clipboard serializer strips the <time> wrapper from the
paste HTML along with the .timestamp-content-wrapper span, losing
the `datetime` attribute needed to reconstruct the `<time:ISO>`
markdown.
We expand the selection range to cover the full <time> element (same
trick the KaTeX path uses to keep its annotation), and at copy time
wrap the localized date text in a fresh `<span data-datetime="...">`.
The paste rule recovers the markdown via the surviving `<time>` tag
(cross-element selections) or the wrapping `<span data-datetime>`
(in-time selections, where Chrome has stripped the <time>).
Fixes: https://chat.zulip.org/#narrow/channel/138-user-questions/topic/Copy-paste-ability.20of.20global.20times/with/2462937
In message_list_view.populate_group_from_message, the tutorial
referred to in the comment for the case when a stream message's
`stream_id` returns an undefined, instead of a StreamSubscription
object, was removed in commit be7f6db854.
Since we now can assert that a StreamSubscription object is
returned for the message in question, we can populate the
MessageGroup fields via the fields in that object, instead of
calling various functions that check for the same object.
Removes the undefined case from stream_data.can_resolve_topics
as well.
zoom_in previously inferred its stream from
topic_list.active_stream_id(), which only works when the topic list
is already active for the target stream. Pass the stream_id in
explicitly so the caller controls which stream is zoomed, and narrow
to that channel's feed first when it isn't already the current
narrow.
Preparation for filtering topics across all channels from the left
sidebar, where zoom-in may target a stream that isn't currently
narrowed.
get_zoomed_topic_search_term reads the zoomed-in topic filter input
(#topic_filter_query), which exists only in the more-topics modal.
Rename it to make that explicit, differentiate it from the upcoming
top-level left sidebar topic search, and assert that it's only
called while zoomed.
filter_topics_left_sidebar runs for both the zoomed topic list and
the inline topic lists under streams, so read its search term from
the zoomed input when zoomed and the left sidebar search box
otherwise, preserving existing behavior. The remaining callers are
all zoomed-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Mark as read" banner click handler previously only marked
messages currently loaded in the message list as read, missing
unreads outside the locally fetched range. For example, in the
DM feed (is:dm), older DM conversations with unreads would not
get marked as read.
Fix this by using the server-side narrow API
(bulk_update_read_flags_for_narrow) to mark every message
matching the current narrow as read, the same pattern used by
mark_stream_as_read and mark_topic_as_read.
This banner can appear in any non-conversation feed view
(channel feed, DM feed, combined feed, mentions, starred,
search, etc.), so the fix applies broadly.
The narrow API was added in b1ca1fd606, after the BUG comment
in 6afdf2410d flagged this limitation.
Reported at: https://chat.zulip.org/#narrow/channel/9-issues/topic/DM.20mark.20all.20as.20read.20only.20marking.20some.20unreads/with/2463479
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a message is re-rendered, we currently don't apply the edit/move
button classes to the newly created DOM element.
In this commit, when a message is re-rendered, the edit/move button
classes are applied.
The empty-right branch of change_state previously set the folder
filter dropdown only when left_side_tab === "all". This came from the
old structure where the folder filter set was buried inside the
section === "all" branch, but it caused a preexisting bug: clicking
"View channels" from a folder popover as a guest passed
left_side_tab = "subscribed" (since guests can't use the "All" tab),
which skipped the folder-filter setup — so guests saw all their
subscribed channels rather than the folder they clicked.
Drop the left_side_tab guard. The folder filter is just a dropdown
value; applying it whenever folder_id is supplied makes the filter
work across all left tabs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
URL slot 1 (`#channels/<slot1>/...`) was overloaded as a left-panel
tab, the create-channel sentinel ("new"), or a channel ID, all passed
to `change_state` as one `section: string`. Parse it at the hashchange
boundary into `right_panel: "new" | number | undefined` and a separate
`left_side_tab`, and dispatch `change_state` on `right_panel` in an
if/else over the three cases.
`right_side_tab` is now strictly the in-channel tab (general /
personal / subscribers / permissions), no longer overloaded with
"new". Opening the create form preserves the user's current left tab
instead of resetting it.
Fixes#27730.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The left-panel toggler had keys "subscribed", "available", and
"all-streams"; the URL hash sections are "subscribed", "available",
and "all". The "all" vs "all-streams" mismatch required a one-line
mapping in URL parsing. Rename the toggler key to "all" so the
toggler and URL hash sections use the same vocabulary.
No functional changes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The folder-create flow at #channels/folders/<id>/new calls change_state
and launch with right_side_tab = "". The create-form branch in
change_state never reads right_side_tab, so this empty string was
vestigial. Pass undefined to match the parameter's `string | undefined`
type and signal "no value" explicitly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Improve alias matching in the Code Playground settings typeahead.
Typing an alias such as py, py3, or python3 now surfaces the canonical
language option instead of suggesting a custom language entry.
This ensures canonical languages are prioritized when matched via aliases.
Fixes#24045.
Filtering across all of a channel's topics (rather than just the
cached ones) requires clicking "Show all topics," which is not
discoverable. This adds a search icon in the expanded channel's
row, to the left of the "+ new topic" button, that opens the
"show all topics" view with the filter input focused — giving
users a clear visual affordance for searching topics within a
channel.
The icon only appears on the expanded channel row, mirroring the
zoom-in mechanism, which only operates on the active channel.
The grid container for the left-sidebar controls now flows in
columns so multiple icons sit side-by-side rather than stacking.
Discussion:
https://chat.zulip.org/#narrow/channel/101-design/topic/Filter.20topics.20within.20channel.20button/with/2369643
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`openInputFieldOnKeyUp` in the search typeahead was used to open the
search input when the user started typing while the search box was
closed.
In commit 75dab825fe, we removed the
ability to type into closed search box, making this code path
unreachable.
We remove this option altogether from bootstrap_typeahead
module.
Tippy schedules show() via setTimeout to honor the hover delay. With
our default appendTo: "parent", the mount path resolves "parent" to
reference.parentNode. If the reference was removed from the DOM
during the delay (e.g., the user hovers a recipient-bar icon, then
narrows away before the 100ms delay elapses, causing the feed to
re-render and detach the icon), parentNode is null and tippy
crashes on parentNode.contains(popper).
Fix this with two complementary changes to the default config:
1. A default onShow that destroys the instance and cancels the show
when the reference is no longer in the DOM. This keeps the DOM
clean: no popper is mounted at all.
2. An appendTo function that falls back to document.body when
reference.parentElement is null. This acts as a structural
backstop, since tippy's onShow is replaced (not composed with) by
per-instance onShow handlers; any tooltip overriding onShow would
otherwise lose the isConnected check.
Reported in Sentry as ~50 events from 41 users over 90 days; the
stack trace lands in the setTimeout callback at the
parentNode.contains line, consistent with this scenario.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 10px radius read overly rounded next to the message feed's
selection box, which uses 5px. Bring `.inbox-row`, `.inbox-header`,
the inbox icon-action focus rings, and `.recent-view-body-row` into
visual consistency with that established pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a row is keyboard-selected but not mouse-hovered, the default-
state action buttons (three-dots menu, default visibility marker) sit
at the tertiary/secondary tier's full reveal opacity, which is louder
than necessary for keyboard navigation: the user has selected the row
but is just orienting, not yet trying to act on it. Drop these to 0.2
in that state so the buttons remain perceivable but recede.
For inbox, the rule scopes to `:focus-within:not(:focus)` (the
row owns focus through children) so mouse hover restores the louder
reveal as soon as the cursor lands. The target elements also carry
`:not(:focus-visible)` so the row-scoped rule yields to a button's
own `:focus-visible` styling once the user arrows into the button —
otherwise the row-scoped selector's higher specificity would clamp
the focused button at 0.2 instead of letting the direct-interaction
rule paint at 0.7.
For recent-view, the rule scopes to `:focus:not(:hover)` rather
than `:focus-visible:not(:hover)`: the existing
`.no-visible-focus-outlines` machinery (see focus_outline_util.ts)
already gates the focus reveal until the first keyboard navigation
key, and `:focus-visible` is fragile for programmatic focus calls
in setTimeout (e.g., the `.trigger("focus")` in
`recent_view_ui.set_table_focus`). Mirror the wrapper-focus (0.7)
and body-row keyboard-focus (0.2) tier rules inside the
`.no-visible-focus-outlines` scope, suppressing icon opacity to 0
while the class is present, so programmatic focus on view entry
doesn't reveal the icons.
Recent-view: the tier selectors match both the natively-rendered
`.recent-view-row-topic-menu` and the JS-swapped vdots (signaled by
`data-vdots-original-icon-class` set in `recent_view_ui`), so
adjacent rows with and without a status marker land at the same
opacity in each interaction state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Topic state markers (follow/mute/unmute) communicate state and so are
secondary information; the three-dots topic menu communicates no state
and is a tertiary action. Replace the previous opacity values — which
left focus dimmer than hover for keyboard users (0 idle → 0.4 row
hover → 0.2 focus on the menu button) — with a consistent tier shared
by both inbox and recent-view:
- Tertiary action (three-dots menu): 0 idle, 0.45 on row reveal,
0.7 on direct hover or focus.
- Secondary state (follow/mute/unmute markers): 0.45 idle,
0.6 on row hover/focus, 0.7 on direct hover.
0.45 — rather than the more obvious 0.4 — is the lowest opacity that
clears WCAG 1.4.11's 3:1 minimum non-text contrast ratio against both
the light (~3.4:1) and dark (~3.9:1) row backgrounds; 0.4 produces
~2.85:1 and fails. The visual difference is imperceptible.
Hover/focus on the icon itself overrides `.recipient_bar_icon`'s
`!important` from `message_header.css`. The inherit-marker icon (the
no-explicit-policy default in inbox) is normalized to the same
opacity values as the explicit-policy markers so all three states
render identically in each tier.
Recent-view: drop the topic-menu wrapper's `outline-offset` from 5px
to 3px: the wrapper sits hard against the recent-view table's right
edge (the table has `overflow: hidden`), so a 5px offset clipped the
outline's right side. The clipping was effectively invisible while
the focused icon was at 0.2 opacity but became apparent once the menu
button moved up to direct-interaction opacity. 3px gives comfortable
clearance while still matching the inbox's 22×22 effective ring size
around a 16×16 icon.
Inbox: several existing rules selected on a parent that doesn't
actually match (e.g., a focus pseudo on the row instead of the
wrapper); rewrite them so they apply.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The focused unread badge's `::before` ring extends `5px`
outside the badge edges, so when the topic name is long
enough to truncate with `...` the ellipsis ends right against
the badge column and the ring's left side paints over the
ellipsis text.
Add `7px` of left padding on
`.recent-view-unread-mention-and-count-wrapper` (5px ring
outset + 1.5px ring stroke + ~1px clearance) so the topic-name
column shrinks just enough to keep `...` clear of the ring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the focus ring on `.unread-count-focus-outline` from a
wrapper-sized `outline` to a pseudo-element on the inner
`.unread_count` badge, matching `recent-view-table-unread-count`'s
pattern: `5px` of breathing room outside the badge with a
`3px` corner radius on the ring. The wrapper-extending outline
left a wide gap between the ring and the badge fill that read
as a second concentric outline; recent's bigger gap reads as
intentional padding around a single badge instead, so the
inbox focus indicator now matches the recent-view treatment
exactly. The badge's inner `.normal-count` `inset` `box-shadow`
is also suppressed on focus so it doesn't sit as a third,
narrower outline inside the ring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the existing CSS-only `background-image`-based hover swap on
`.visibility-status-icon` with a JavaScript class swap that fires on
hover, keyboard focus of the focusable wrapper, or while the topic
popover is open. The wider trigger set gives keyboard users the same
affordance — "this opens the topic-actions popover" — that mouse
users get on hover, and the sticky popover-open case keeps the icon
linked to the open menu after hover/focus moves into the popover.
The swap toggles the icon-font class itself (e.g.,
`zulip-icon-follow` → `zulip-icon-more-vertical`) and remembers the
original on a data attribute so leave events restore it. Doing this
in JS rather than via `mask-image: url(...)` matters for visual
fidelity: the icon font normalizes `more-vertical.svg` into a
shared em-square that renders the glyph at smaller dimensions than
a direct `mask-size: contain` paint of the same SVG, so a CSS
swap produced visibly different vdots from the row's natively
rendered `zulip-icon-more-vertical` siblings. Routing both states
through the icon font guarantees identical rendering.
The focus trigger gates on the recent-view's own
`no-visible-focus-outlines` flag — the same one that suppresses
focus rings until the first keyboard navigation key (see
`focus_outline_util`). Browser-native `:focus-visible` can match
on programmatic focus that inherits keyboard mode from a prior
element (sidebar tab-then-click, scroll-driven row re-focus on
view entry), and we don't want a surprising vdots reveal in those
cases — the row is focused but the user hasn't acted on the
keyboard yet. `change_focused_element` re-evaluates the swap
state once the flag clears so the first keyboard nav reveals the
glyph alongside the focus ring.
Drop the now-obsolete `filter: invert(1)` hover rule from
`dark_theme.css`: it existed solely to recolor a previously
hard-coded-black `background-image` glyph for dark theme. The
icon font already honors `color` and renders white-on-dark.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the inbox `.unread-count-focus-outline` 2px outline with a
1.5px outline insetted 2px and rounded at 5px — matching the
action button and state-marker focus rings (also -2px on a 30px
wrapper) so all four inbox focus indicators read at the same 26px
outer height. Suppress the inner badge's `inset` `box-shadow`
(from `.normal-count`) so it doesn't read as a second, narrower
outline inside the focus ring.
For recent-view, draw the badge focus ring as a `::before`
pseudo-element with inset `-5px` and a 5px radius, matching the
breathing room the inbox wrapper-extending outline gets from its
`padding: 0 5px`. Scope the rule via
`#recent_view:not(.no-visible-focus-outlines)` so the badge ring
stays suppressed alongside the wrapper outline until the first
keyboard navigation key removes the class (see focus_outline_util.ts),
and so the rule has high enough specificity (1,2,1) to beat the
`#recent_view .recent_view_focusable:focus` wrapper rule (1,1,1).
Without that specificity, both rings would paint.
A subsequent commit tightens the inbox ring further so it hugs
the inner badge instead of the row-tall wrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The three icon-style row controls — `.channel-visibility-policy-indicator`,
`.visibility-policy-indicator`, and `.inbox-action-button` — had three
different sizes, click areas, focus-ring colors, and corner radii.
Unify them as 30×30 (row-tall) flex wrappers around their 16×16 icons,
with `cursor: pointer` carried by the wrapper so the entire hit area
feels clickable, and a single shared focus-ring style.
The focus ring lives on the wrapper at full opacity to satisfy
WCAG 1.4.11's 3:1 contrast requirement for focus indicators (an
earlier draft drew the outline on the inner icon inside a 0.7-opacity
wrapper, which dimmed the stroke via opacity inheritance to ~2.7:1 in
light / ~2.4:1 in dark). Negative `outline-offset: -2px` keeps the
1.5px stroke inside the wrapper's 30×30 bounds so it doesn't clip
against the row's `overflow: hidden` edge, and a 10px border-radius
matches the row outline so the button ring visibly rounds at 1× DPR
rather than reading as square. The inner icon dims separately at 0.7
on focus so the button still reads as subordinate to row content.
Icon color is unified across all three wrappers as well: action button
icons inherit the same color as the state-marker icons rather than
their previous distinct shade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit gave `.inbox-row` and `.inbox-header` identical
`:focus-visible` outline rules. Merge them into one combined
`.inbox-row, .inbox-header` block, alongside the existing icon
visibility rule that already targeted both selectors. The
header-only `.collapsible-button` rules move to a separate
`.inbox-header:focus-visible` block.
No behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the 2px inner-border focus mechanism on `.inbox-focus-border`
with a 1.5px outline on `.inbox-row` and `.inbox-header` themselves,
matching `.recent-view-body-row`. Unlike recent-view rows, inbox
rows are tighter (30px), so the outline sits flush with the row
edges rather than insetting 4px. Drop the DM row's separate inset
box-shadow since the unified outline works for multi-line rows
too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring `#inbox-filter_widget` and `#recent_view_folder_filter_button`'s
focus outlines from 2px down to 1.5px, matching the row focus
outline width used elsewhere in the same views.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull the `var(--focus-ring-width-in-views) solid
var(--color-outline-focus)` shorthand into a single variable
`--focus-ring-outline` so a future width or color change touches
one place. Convert the one pre-existing usage on
`.recent_view_focusable:focus`; upcoming commits in this branch
introduce six more usages (five `outline:` shorthands and one
`border:` on the `.recent-view-table-unread-count::before`
pseudo-element) that use the variable directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull the 1.5px focus-ring stroke width used by the inbox and
recent views into `--focus-ring-width-in-views` so a future tweak
touches one place. Convert the existing `outline-width: 1.5px`
declarations on the unread-badge hover-outline animation in both
views (and on the inbox row's hover-forwarded badge), the
pre-existing `.recent_view_focusable:focus` outline, and prepare
for upcoming commits that introduce additional 1.5px focus rings.
Scoped to the row-list views: focus widths elsewhere in the
codebase vary (1px in popovers/modals, 2px in the left sidebar
and message feed), so the variable name carries `-in-views` to
signal it isn't a global focus width.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The unread-count badge in recent view is keyboard-focusable, and
its Tippy tooltip fires on both mouse hover and keyboard focus by
default. The latter pops a redundant 'Mark as read' bubble every
time the user arrows into the badge, which competes with the
focus ring as the visual cue.
Add `data-tippy-trigger="mouseenter"` so the tooltip only
appears on hover. Inbox unread badges don't have this issue
because their focusable wrapper is the parent of the tooltipped
badge, so keyboard focus on the wrapper doesn't trigger the
inner badge's tooltip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the 3-dots topic menu or stream/visibility-policy popover
is open, the trigger button should stay in the direct-interaction
tier (0.7) rather than fading back to hidden/quiet when the
cursor leaves it. Otherwise there's no visual link between the
open popover and the row it belongs to.
Inbox: add `.visibility-policy-popover-visible` and
`.topic-popover-visible` opacity rules for the state-marker
wrappers and the action button. The former class is already
toggled by user_topic_popover.ts; extend topic_popover.ts to
toggle the latter on `.inbox-action-button` too (it previously
only tagged `.recent_view_focusable`, so inbox got nothing), and
extend stream_popover.ts to do the same for inbox channel
headers, where the trigger is also an `.inbox-action-button`.
Recent: the existing topic-popover-visible rule is already in
its final form on the wrapper from the prior commit, so this
commit only adds the JS plumbing for inbox.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the `#inbox-list` unread click handler from the inner
`.unread_count` badge to the row-tall `.unread-count-focus-outline`
wrapper, so clicking anywhere in the unread-count column marks
the conversation as read — not just the small grey pill. Data
attributes still live on the inner badge; the handler reads them
via `.find()`. Merges the previous `.on_hover_dm_read` and
`.on_hover_topic_read` handlers into a single one.
Carry `cursor: pointer` and forward the wrapper's `:hover` to
the badge's hover-outline animation so hovering anywhere in the
new click target previews the click affordance, not just hovering
directly over the inner pill.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both views previously showed the 'Mark as read' tooltip on the
unread-count badge with tippy's default zero delay: hover the
badge for any duration and the bubble appears instantly,
covering nearby UI on every casual mouse-over.
Bring both into line with the project's existing
`LONG_HOVER_DELAY` (750 ms) used for similar
hover-to-explain tooltips:
- Inbox templates switch from `tippy-zulip-tooltip` to
`tippy-zulip-delayed-tooltip` on the unread badges, picking
up the delegate at `tippyjs.ts:156` that already configures
`delay: LONG_HOVER_DELAY`.
- Recent's per-row `tippy.delegate` adds `delay: LONG_HOVER_DELAY`
alongside its existing per-row placement logic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
We pass entire stream object context when rendering channel_list_item.hbs
which is then used by stream_privacy and renders the archived
icon correctly.
Currently, all streams in the add channel to folder widget list in
channel folder ui modal are sorted alphabetically, including archived
streams. Instead, first sort non-archived streams alphabetically and
then place archived streams, also sorted alphabetically, at the bottom
of the list.
Currently, all streams in the user channel subscribe widget list in
user profile modal are sorted alphabetically, including archived
streams. Instead, first sort non-archived streams alphabetically and
then place archived streams, also sorted alphabetically, at the bottom
of the list.
We define a helper function compare_stream_by_name in util.ts to
sort the streams.
We use the locale-aware strcmp (backed by Intl.Collator) to sort
channels in the user channel subscribe widget, matching how channels
are sorted elsewhere in the codebase.
The previous sort lowercased both names before comparing, making it
case-insensitive. strcmp uses Intl.Collator's default sensitivity, so
sort order may occasionally differ for channel names that differ only
in case or contain non-ASCII characters.
Add `padding-right` to `.selected-stream`/`.selected-group` so the
inset focus ring no longer hugs the channel/group name and so it
sits clear of the adjacent edit button's focus ring. Also drop
the underline on `.selected-stream:focus-visible`, since the
outline already indicates focus.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The keyboard focus outlines on the channel-name link and the
title-row action buttons (edit, preview, archive, subscribe) in
the channel and user-group settings overlay were being clipped at
the top and bottom, and rendered in the black/white
`--color-outline-button-focus` rather than the regular focus blue.
The clipping is caused by `overflow: hidden` on the .display-type
and .stream-info-title ancestors, which clip the outside-drawn
outlines on `.selected-stream`, `.icon-button`, and
`.action-button`. Inset the outlines with `outline-offset: -2px`
and recolor them to `--color-outline-focus` so they sit fully
within the focusable elements and match the focus styling used
elsewhere in the app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the universal h1, h2, h3, h4 selectors carried over from
Bootstrap 2.3.2. Earlier commits in this series made every scoped
heading rule that depended on these resets self-sufficient, so this
deletion is purely a no-op for visible behavior.
Removed properties were:
* margin: 10px 0 (now set explicitly on .overlay-messages-header h1,
.settings-header h1, .settings-section h3,
.drafts-list h2 / .message-edit-history-list h2,
.empty-feed-notice-title, .poll-question-header /
.todo-task-list-title-header, and
.group-assigned-permissions .group-permissions-section > h3)
* line-height: 20px / var(--header-line-height) (now set explicitly
on the same rules where it was load-bearing — overlay headers,
user-profile modal h3s, folder-channels-list-header, etc.)
* font-size: 38.5px/31.5px/24.5px/17.5px (overridden in every place
the app actually rendered these headings; only one bare instance
per overlay needed an explicit replacement, captured above)
* font-weight: bold (matches the user-agent default for h1-h6 — no
replacement needed; .light's 'font-weight: 300' override now
competes only with the UA default, preserving its current effect)
* color: inherit, font-family: inherit, text-rendering:
optimizelegibility (no-ops vs. UA defaults; no replacement needed)
The .alert h4 rules at the bottom of the file are part of the alert
component, not the heading reset, and remain in place.
The --header-line-height variable in app_variables.css is still used
by modal.css (height: calc(100% - var(--header-line-height))) and by
several of the explicit replacements above, so it stays.
Verified with a Puppeteer-based computed-style sweep: settings
overlay (Account, Notifications, Preferences, Org permissions, Org
profile), drafts and scheduled-messages overlay headers, and the
empty-feed notice all render identical font-size, line-height,
margin, font-weight, and bounding-box dimensions before and after
this series.
The <h4 class="stream_setting_subsection_title"> and
<h4 class="user_group_setting_subsection_title"> headings in
channel and group settings rely on the universal h1-h4 reset in
bootstrap.app.css for 'margin: 10px 0'. With display: inline-block,
vertical margins apply, and the bootstrap-supplied 10px is
visible spacing above/below these subsection titles.
Set 'margin: 10px 0' explicitly on the combined rule so both
classes keep their top and bottom margin once the reset is
removed. The 'margin-bottom: 5px' override for the stream
variant is preserved by moving it below the combined rule, so it
wins by source order.
No visual change.
The <h4 class="left-sidebar-title"> elements in the left sidebar
(rendered from left_sidebar.hbs and stream_list_section_container.hbs)
rely on the universal h1-h4 reset in bootstrap.app.css for
'line-height: 20px'. The existing rule overrides margin, font-size,
and font-weight explicitly, but not line-height — so once the
reset is removed, the h4 would inherit its line-height from the
surrounding grid cell, which may not match.
Set 'line-height: 20px' explicitly so the title's vertical metrics
survive the bootstrap reset's removal.
No visual change.
The "Permissions" tab of a user group's settings (rendered from
group_permission_settings.hbs) has three bare <h3> section titles —
"Organization permissions", "Channel permissions", "User group
permissions" — that sit directly inside .realm-group-permissions /
.channel-group-permissions / .user-group-permissions (all of which
also carry the shared .group-permissions-section class). They
currently rely on the universal h1-h4 reset in bootstrap.app.css for
font-size, line-height, and margin.
Add an explicit rule scoped to '.group-assigned-permissions
.group-permissions-section > h3' so these titles keep their current
appearance once the reset is removed. font-weight is left at the
user-agent default (bold) — same as bootstrap was supplying.
Note that the new font-size is expressed in em (1.5313em ≈ 24.5px
at the 16px base font) rather than the absolute 24.5px that the
bootstrap reset supplied. This means the section title now scales
with the user's chosen information-density font size rather than
staying fixed at 24.5px regardless. At the default 16px base font
the rendered size is identical; at non-default font densities it
shifts proportionally. A small deliberate improvement, not a
strict "no visual change" preservation.
The pre-existing .subsection-header h3 rule (line 458) covers the
per-subsection titles but did not set margin, relying on the
bootstrap reset's 'margin: 10px 0'. Since .subsection-header is
'display: flex', the h3 inside is a flex item with blockified
display, so those vertical margins were active — removing the
bootstrap reset would have collapsed the subsection header
vertically. Add 'margin: 10px 0' explicitly to that rule as well.