@redsift/products 12.5.9 → 12.5.10-muiv8-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ import styled, { css } from 'styled-components';
5
5
  import pako from 'pako';
6
6
  import { interpolateTranslation, mapStatusToDetailedCardColor, isStructuredData, calculateSankeyHeight, create2LevelSankey, extractDmarcData, extractSpfData, extractTlsData, extractFcrdnsData, extractDnssecData, extractThreatIntelligenceData, extractUrlsData, extractBimiData, extractAmpData, extractSubdoData, extractDomainAnalyzerDkimData, extractDomainAnalyzerSpfData, extractDmarcDomainData, extractSpfDomainData, extractMtaStsFullData, extractDkimData, recordStatus } from '@redsift/signal-logic';
7
7
  import { useLocalizedStringFormatter, useTheme, Button, Icon, DetailedCard, ButtonLink, Flexbox } from '@redsift/design-system';
8
- import { mdiHelpCircle, mdiClose, mdiAlert, mdiEmail, mdiEarth, mdiInformationOutline, mdiCheck, mdiInformation } from '@redsift/icons';
8
+ import { mdiHelpCircle, mdiClose, mdiAlert, mdiEmail, mdiEarth, mdiInformationOutline, mdiCheck, mdiInformation, mdiFormatAlignRight, mdiFormatAlignJustify } from '@redsift/icons';
9
9
  import { createPortal } from 'react-dom';
10
10
 
11
11
  const COLOR = {
@@ -283,6 +283,7 @@ var EXTRACTOR_STRINGS = {
283
283
  "card.dkim.unavailable-try-later_md": "Please try sending another email later.",
284
284
  "card.dkim.unavailable_md": "**[](DKIM#:glossary) evaluation temporarily unavailable.**",
285
285
  "card.dmarc.domain-aligned": "The domain is aligned with the From Domain. It can contribute to passing DMARC.",
286
+ "card.dmarc.domain-aligned-not-verified": "The domain is aligned with the From Domain, but its signature did not verify, so it cannot contribute to passing DMARC.",
286
287
  "card.dmarc.domain-not-aligned": "The domain is not aligned with the From Domain. It cannot contribute to passing DMARC.",
287
288
  "card.dmarc.error.dns-timeout": "DNS timeout when looking up DMARC record",
288
289
  "card.dmarc.error.invalid-pct-value": "Pct value must be between 0 and 100",
@@ -766,6 +767,7 @@ var EXTRACTOR_STRINGS = {
766
767
  "card.dkim.unavailable-try-later_md": "Veuillez essayer d'envoyer un autre courriel plus tard.",
767
768
  "card.dkim.unavailable_md": "**[](DKIM#:glossary) \xE9valuation temporairement indisponible.**",
768
769
  "card.dmarc.domain-aligned": "Le domaine est align\xE9 avec le domaine exp\xE9diteur. Il peut contribuer \xE0 la r\xE9ussite de DMARC.",
770
+ "card.dmarc.domain-aligned-not-verified": "Le domaine est align\xE9 avec le domaine exp\xE9diteur, mais sa signature n'a pas \xE9t\xE9 v\xE9rifi\xE9e, il ne peut donc pas contribuer \xE0 la r\xE9ussite de DMARC.",
769
771
  "card.dmarc.domain-not-aligned": "Le domaine n'est pas align\xE9 avec le domaine exp\xE9diteur. Il ne peut pas contribuer \xE0 la r\xE9ussite de DMARC.",
770
772
  "card.dmarc.error.dns-timeout": "D\xE9lai d'attente DNS lors de la recherche de l'enregistrement DMARC",
771
773
  "card.dmarc.error.invalid-pct-value": "La valeur pct doit \xEAtre entre 0 et 100",
@@ -1334,7 +1336,7 @@ var enUS = {
1334
1336
  "investigate.card.dmarc.void-dynamic-dmarc-ns-record": "Dynamic DMARC record (NS) is added but Dynamic Services aren't enabled",
1335
1337
  "investigate.card.dmarc.policy-none-pct-0_md": "No reports will be sent with this policy, because the sample rate [](PCT#:glossary) is set to `0`.",
1336
1338
  "investigate.card.dmarc.policy-not-none-pct-0_md": "All **unauthorized** email will be delivered to receivers although the policy is set to [{policy}](DMARC_POLICY#:glossary), because the sample rate [](PCT#:glossary) is set to `0`.",
1337
- "investigate.card.dmarc.policy-inherited-from-org-domain_md": "The DMARC policy for the [](FROM#:glossary) `{fromDomain}` comes from the [](ORGANIZATIONAL_DOMAIN#:glossary) `{orgDomain}`",
1339
+ "investigate.card.dmarc.policy-inherited-from-org-domain_md": "The DMARC policy for the [](FROM#:glossary) `{fromDomain}` comes from the [Organizational Domain](ORGANIZATIONAL_DOMAIN#:glossary) `{orgDomain}`",
1338
1340
  "investigate.card.dmarc.category-policy-good_md": "The policy on your DNS conforms with the DMARC specification.",
1339
1341
  "investigate.card.dmarc.all-emails-reported_md": "All emails will be [REPORTED](DMARC_POLICY#:glossary).",
1340
1342
  "investigate.card.dmarc.all-emails-reported-only_md": "DMARC configured in report-only mode. All emails will only be [REPORTED](DMARC_POLICY#:glossary).",
@@ -1924,6 +1926,7 @@ var enUS = {
1924
1926
  "investigate.section-titles.bimi-domain": "DMARC Domains",
1925
1927
  "investigate.section-titles.dmarc-domain": "DMARC Domains",
1926
1928
  "investigate.card.dmarc.domain-aligned": "The domain is aligned with the From Domain. It can contribute to passing DMARC.",
1929
+ "investigate.card.dmarc.domain-aligned-not-verified": "The domain is aligned with the From Domain, but its signature did not verify, so it cannot contribute to passing DMARC.",
1927
1930
  "investigate.card.dmarc.domain-not-aligned": "The domain is not aligned with the From Domain. It cannot contribute to passing DMARC."
1928
1931
  };
1929
1932
 
@@ -2022,11 +2025,11 @@ var frFR = {
2022
2025
  "investigate.card.dmarc.policy-remaining-emails-pct-reject_md": "Les **{remainingPercent}%** restants de courriels non autorisés seront soumis à la politique de **mise en quarantaine**.",
2023
2026
  "investigate.card.dmarc.policy-remaining-emails-pct-quarantine_md": "Les **{remainingPercent}%** restants de courriels non autorisés seront **livrés normalement**.",
2024
2027
  "investigate.card.dmarc.no-policy_md": "**Aucune [FULL](DMARC_POLICY#:glossary) publiée sur le domaine**\n\n**`{policyDomain}`**",
2025
- "investigate.card.dmarc.dmarc-passed_md": "[](DMARC#:glossary) **passé**",
2026
- "investigate.card.dmarc.dmarc-passed-no-rua_md": "[](DMARC#:glossary) est conforme au BIMI **MAIS** il n'a pas d'entrée **aggregate report address** (rua).",
2027
- "investigate.card.dmarc.dmarc-no-rua_md": "[](DMARC#:glossary) n'a pas d'entrée **aggregate report address** (rua).",
2028
+ "investigate.card.dmarc.dmarc-passed_md": "[](DMARC#:glossary) **réussi**",
2029
+ "investigate.card.dmarc.dmarc-passed-no-rua_md": "[](DMARC#:glossary) est conforme à BIMI **MAIS** n'a pas d'**adresse de rapport agrégé** (rua).",
2030
+ "investigate.card.dmarc.dmarc-no-rua_md": "[](DMARC#:glossary) n'a pas d'**adresse de rapport agrégé** (rua).",
2028
2031
  "investigate.card.dmarc.dmarc-invalid-record_md": "[](DMARC#:glossary) a un enregistrement **invalide** .",
2029
- "investigate.card.dmarc.dmarc-failed_md": "[](DMARC#:glossary) **ne sont pas** conformes au BIMI",
2032
+ "investigate.card.dmarc.dmarc-failed_md": "Les paramètres [](DMARC#:glossary) **ne sont pas** conformes à BIMI",
2030
2033
  "investigate.card.dmarc.misplaced-policy_md": "**Le [FULL](DMARC_POLICY#:glossary) est mal placé.",
2031
2034
  "investigate.card.dmarc.misplaced-bad-policy_md": "**Le [FULL](DMARC_POLICY#:glossary) est mal placé et a une erreur de syntaxe:**",
2032
2035
  "investigate.card.dmarc.bad-policy_md_one": "**Le [FULL](DMARC_POLICY#:glossary) présente l'erreur de syntaxe suivante:**.",
@@ -2049,7 +2052,7 @@ var frFR = {
2049
2052
  "investigate.card.dmarc.void-dynamic-dmarc-ns-record": "L'enregistrement DMARC dynamique (NS) est ajouté mais les Dynamic Services ne sont pas activés.",
2050
2053
  "investigate.card.dmarc.policy-none-pct-0_md": "Aucun rapport ne sera envoyé avec cette politique, car le taux d'échantillonnage [](PCT#:glossary) est fixé à `0`.",
2051
2054
  "investigate.card.dmarc.policy-not-none-pct-0_md": "Tous les emails **non autorisés** seront délivrés aux destinataires bien que la politique soit définie sur [{policy}](DMARC_POLICY#:glossary), parce que le taux d'échantillonnage [](PCT#:glossary) est défini sur `0`.",
2052
- "investigate.card.dmarc.policy-inherited-from-org-domain_md": "La politique DMARC du [](FROM#:glossary) `{fromDomain}` provient du [](ORGANIZATIONAL_DOMAIN#:glossary) `{orgDomain}`",
2055
+ "investigate.card.dmarc.policy-inherited-from-org-domain_md": "La politique DMARC du [](FROM#:glossary) `{fromDomain}` provient du [domaine organisationnel](ORGANIZATIONAL_DOMAIN#:glossary) `{orgDomain}`",
2053
2056
  "investigate.card.dmarc.category-policy-good_md": "La politique de votre DNS est conforme à la spécification DMARC.",
2054
2057
  "investigate.card.dmarc.all-emails-reported_md": "Tous les courriels seront [REPORTED](DMARC_POLICY#:glossary).",
2055
2058
  "investigate.card.dmarc.all-emails-reported-only_md": "DMARC configuré en mode rapport uniquement. Tous les courriels seront uniquement [REPORTED](DMARC_POLICY#:glossary).",
@@ -2104,21 +2107,21 @@ var frFR = {
2104
2107
  "investigate.card.subdo.blog": "Vous pouvez obtenir un aperçu complet de votre configuration SPF sur notre vérificateur SPF, ou en savoir plus sur l'attaque SubdoMailing sur notre base de connaissances.",
2105
2108
  "investigate.card.subdo.spf-subdo-poisoned-include_md": "Inclusion compromise : **{raw}**",
2106
2109
  "investigate.card.subdo.spf-subdo-poisoned-mechanism_md": "Inclusion compromise : **{raw}**",
2107
- "investigate.card.spf.spf-no-spf_md": "Pas de SPF configuré pour le domaine.",
2110
+ "investigate.card.spf.spf-no-spf_md": "Aucun SPF configuré pour le domaine.",
2108
2111
  "investigate.card.spf.spf-ipf-error-spf_md": "Erreur SPF : {msg}",
2109
2112
  "investigate.card.spf.spf-extra-spf-lookups_md": "Trop de recherches SPF ({lookupCount} trouvées, 10 autorisées)",
2110
2113
  "investigate.card.spf.spf-multiple-mx-providers_md": "Plusieurs fournisseurs de messagerie configurés dans les enregistrements MX de ce domaine.",
2111
- "investigate.card.spf.spf-multiple-spf_md": "Plusieurs enregistrements SPF ont été trouvés pour ce domaine",
2112
- "investigate.card.spf.spf-multiple-all_md": "Plusieurs mécanismes **tous** dans l'enregistrement SPF",
2113
- "investigate.card.spf.spf-recursive-include-spf_md": "L'inclusion SPF redondante peut être supprimée car elle renvoie à votre propre domaine.",
2114
- "investigate.card.spf.spf-v-tag-duplicated-spf_md": "La balise de la version est dupliquée.",
2115
- "investigate.card.spf.spf-all-not-last_md": "Toute donnée située après le **tout** est ignorée.",
2116
- "investigate.card.spf.spf-all-and-redirect_md": "Toute **redirection** est ignorée s'il existe un mécanisme **tous**.",
2117
- "investigate.card.spf.spf-all-plus-warning_md": "Le mécanisme **+all** ne doit pas être utilisé car il autorise toutes les adresses IP d'envoi.",
2114
+ "investigate.card.spf.spf-multiple-spf_md": "Plusieurs enregistrements SPF trouvés pour ce domaine",
2115
+ "investigate.card.spf.spf-multiple-all_md": "Plusieurs mécanismes **all** dans l'enregistrement SPF",
2116
+ "investigate.card.spf.spf-recursive-include-spf_md": "L'inclusion SPF redondante peut être supprimée car elle pointe vers votre propre domaine",
2117
+ "investigate.card.spf.spf-v-tag-duplicated-spf_md": "La balise de version est dupliquée.",
2118
+ "investigate.card.spf.spf-all-not-last_md": "Toute donnée après le **all** est ignorée.",
2119
+ "investigate.card.spf.spf-all-and-redirect_md": "Toute **redirection** est ignorée s'il y a un mécanisme **all**.",
2120
+ "investigate.card.spf.spf-all-plus-warning_md": "Le mécanisme **+all** ne devrait pas être utilisé car il autorise toutes les adresses IP d'envoi.",
2118
2121
  "investigate.card.spf.spf-all-question-mark-warning_md": "**?all** ne doit pas être utilisé car il permet une réponse ambiguë.",
2119
- "investigate.card.spf.spf-no-all_md": "Il est recommandé d'utiliser un modificateur **redirect** ou un mécanisme **all** pour mettre fin explicitement au traitement.",
2120
- "investigate.card.spf.spf-invalid-spf1-header_md": "Enregistrement SPF non valide",
2121
- "investigate.card.spf.spf-invalid-spf-header-include_md": "L'enregistrement SPF n'est pas valide car il contient un espace entre la déclaration include :.",
2122
+ "investigate.card.spf.spf-no-all_md": "Il est recommandé d'utiliser soit un modificateur **redirect** soit un mécanisme **all** pour terminer explicitement le traitement.",
2123
+ "investigate.card.spf.spf-invalid-spf1-header_md": "Enregistrement SPF invalide",
2124
+ "investigate.card.spf.spf-invalid-spf-header-include_md": "L'enregistrement SPF est invalide car il a un espace entre l'instruction include:.",
2122
2125
  "investigate.card.spf.spf-ip-subnet-mask-too-small_md": "Le sous-réseau **{raw}** permet à un grand nombre d'hôtes d'envoyer du courrier électronique au nom du domaine.",
2123
2126
  "investigate.card.spf.spf-ip-subnet-overlaps-public-range_md": "Le sous-réseau **{raw}** chevauche la plage **{value}**, appartenant à un fournisseur de cloud accessible au public : **{sourceRef}**.",
2124
2127
  "investigate.card.spf.spf-directive-permissive-exists_md": "Le mécanisme **{raw}** a été défini pour permettre à n'importe quel hôte d'envoyer un courrier électronique au nom du domaine.",
@@ -2641,6 +2644,7 @@ var frFR = {
2641
2644
  "investigate.section-titles.bimi-domain": "Domaines DMARC",
2642
2645
  "investigate.section-titles.dmarc-domain": "Domaines DMARC",
2643
2646
  "investigate.card.dmarc.domain-aligned": "Le domaine est aligné avec le domaine expéditeur. Il peut contribuer à la réussite de DMARC.",
2647
+ "investigate.card.dmarc.domain-aligned-not-verified": "Le domaine est aligné avec le domaine expéditeur, mais sa signature n'a pas été vérifiée, il ne peut donc pas contribuer à la réussite de DMARC.",
2644
2648
  "investigate.card.dmarc.domain-not-aligned": "Le domaine n'est pas aligné avec le domaine expéditeur. Il ne peut pas contribuer à la réussite de DMARC."
2645
2649
  };
2646
2650
 
@@ -6982,6 +6986,206 @@ function renderList(items) {
6982
6986
  }, item)));
6983
6987
  }
6984
6988
 
6989
+ // =============================================================================
6990
+ // STATUS LIST RENDERING
6991
+ // =============================================================================
6992
+
6993
+ /**
6994
+ * Status-list container. Mirrors the plain `<ul>` that `renderList` emits so the
6995
+ * two list variants sit at the same indent and rhythm, adding only the per-entry
6996
+ * row layout the status icons need.
6997
+ */
6998
+ const StyledStatusList = styled.ul`
6999
+ li {
7000
+ display: flex;
7001
+ align-items: center;
7002
+ gap: 0.35em;
7003
+ min-width: 0;
7004
+ }
7005
+ `;
7006
+
7007
+ /**
7008
+ * The entry's text (a domain, on the alignment row).
7009
+ *
7010
+ * `min-width: 0` has to be on the text itself, not just the `li`: a flex item
7011
+ * defaults to `min-width: auto`, which refuses to shrink below its content's
7012
+ * intrinsic width, so a long unbroken domain would push the status icon
7013
+ * (`flex-shrink: 0`) out of the row. `overflow-wrap: anywhere` lets a label with no
7014
+ * natural break opportunity wrap instead of overflowing.
7015
+ *
7016
+ * A dedicated component rather than an `li > span` descendant selector, which would
7017
+ * also be matched by the strike wrapper if the markup were ever reshuffled.
7018
+ */
7019
+ const StyledStatusListText = styled.span`
7020
+ min-width: 0;
7021
+ overflow-wrap: anywhere;
7022
+ `;
7023
+
7024
+ // A div, not a span: TooltipInPortalWrapper renders a <div> (TooltipInPortal.tsx),
7025
+ // and a <div> inside a <span> is invalid HTML — span is phrasing content, div is
7026
+ // flow content. `display: inline-flex` keeps it laid out inline regardless. This
7027
+ // matches the row-level alignment icon in SignalCardExtractedItems, which wraps
7028
+ // the same tooltip in a div for the same reason.
7029
+ const StyledStatusListIcon = styled.div`
7030
+ display: inline-flex;
7031
+ align-items: center;
7032
+ flex-shrink: 0;
7033
+ `;
7034
+
7035
+ /**
7036
+ * Diagonal strike across a status icon, for the not-aligned case.
7037
+ *
7038
+ * Drawn in CSS over the mdi glyph rather than inlining a bespoke crossed-out SVG,
7039
+ * so it scales with the glyph and the DS keeps a single source for the underlying
7040
+ * icon.
7041
+ *
7042
+ * The colour is declared **here, once**, on the wrapper: the `::after` gradient
7043
+ * picks it up through `currentColor`, and the `Icon` inside is passed
7044
+ * `currentColor` too, so glyph and strike cannot drift apart. Setting it on the
7045
+ * pseudo-element instead would make `currentColor` resolve to that local
7046
+ * declaration rather than being inherited, which is the trap this replaced.
7047
+ */
7048
+ const StyledStruckThrough = styled.span`
7049
+ position: relative;
7050
+ display: inline-flex;
7051
+ align-items: center;
7052
+ color: ${COLOR.main.red};
7053
+
7054
+ &::after {
7055
+ content: '';
7056
+ position: absolute;
7057
+ inset: 0;
7058
+ background: linear-gradient(
7059
+ to top right,
7060
+ transparent calc(50% - 0.7px),
7061
+ currentColor calc(50% - 0.7px),
7062
+ currentColor calc(50% + 0.7px),
7063
+ transparent calc(50% + 0.7px)
7064
+ );
7065
+ pointer-events: none;
7066
+ }
7067
+ `;
7068
+
7069
+ /**
7070
+ * Status icon for a single {@link StatusListItem}.
7071
+ *
7072
+ * Mirrors the icon vocabulary `getIcon` uses for row-level statuses in
7073
+ * SignalCardExtractedItems, so a per-entry tick reads identically to a row-level
7074
+ * one. A `''` status means "no status" — the entry renders as plain text, exactly
7075
+ * as a `ListData` entry would.
7076
+ *
7077
+ * Per Antony's spec (2026-08-06) this row uses an **align** glyph rather than a
7078
+ * generic tick/cross, with the not-aligned case struck through:
7079
+ *
7080
+ * | status | `matchMode` | meaning | render |
7081
+ * | --- | --- | --- | --- |
7082
+ * | `good`/`warning` | `exact` | signing domain equals the From domain | green `FormatAlignJustify` |
7083
+ * | `good`/`warning` | `organizational` | shares the Organizational Domain | green `FormatAlignRight` |
7084
+ * | `danger` | — | does not align | red `FormatAlignRight`, struck through |
7085
+ *
7086
+ * **`warning` renders as aligned, not amber.** This row is about *domains*, not
7087
+ * mechanism results (Antony, option (a), 2026-08-06). signal-logic's `warning`
7088
+ * branch fires on `aligned && !verified`, where `verified` is
7089
+ * `signature.code === 'pass'` — a property of the crypto, not of the domain pair —
7090
+ * and the DKIM card already reports verification per signature. Colouring it
7091
+ * differently here would make one icon answer two questions and duplicate the DKIM
7092
+ * card, inviting the two to disagree. The distinct `tooltipKey`
7093
+ * (`card.dmarc.domain-aligned-not-verified`) is preserved, so the detail is still
7094
+ * available on hover.
7095
+ *
7096
+ * The known cost: an aligned-but-unverified domain cannot actually contribute to
7097
+ * DMARC passing, so green slightly overstates it. That is conveyed by the
7098
+ * "Alignment Check" prose rows and the overall card verdict, which read as failed —
7099
+ * the honest place for it, rather than overloading a per-domain icon.
7100
+ *
7101
+ * **Exact vs organizational** comes from `matchMode`, added upstream by WEB-1203.
7102
+ * An exact match is *inherently* aligned under either `adkim` mode, so it earns the
7103
+ * fully-justified glyph; an Organizational Domain match only aligns under `adkim=r`
7104
+ * and gets the right-aligned one. `matchMode` is absent on non-aligned entries and
7105
+ * on the fallback `ListData` shape, so a missing value degrades to `FormatAlignRight`
7106
+ * rather than guessing.
7107
+ */
7108
+ function renderItemStatusIcon(status, matchMode, accessibleName) {
7109
+ // The DS `Icon` is decorative by default — it sets aria-hidden unless given an
7110
+ // aria-label. The status here is not decorative: it is the only thing telling a
7111
+ // reader whether the domain aligned, and the tooltip carrying that meaning is
7112
+ // hover-only. So pass the translated tooltip text as the accessible name, with
7113
+ // `aria-hidden={false}` to un-hide it, and fall back to the bare status word
7114
+ // when an entry supplies no tooltipKey.
7115
+ const label = accessibleName !== null && accessibleName !== void 0 ? accessibleName : status || undefined;
7116
+ const aria = label ? {
7117
+ 'aria-label': label,
7118
+ 'aria-hidden': false
7119
+ } : {};
7120
+ switch (status) {
7121
+ // `warning` is deliberately identical to `good` — see above.
7122
+ case 'good':
7123
+ case 'warning':
7124
+ return /*#__PURE__*/React__default.createElement(Icon, _extends({
7125
+ icon: matchMode === 'exact' ? mdiFormatAlignJustify : mdiFormatAlignRight,
7126
+ color: COLOR.main.green,
7127
+ size: "small"
7128
+ }, aria));
7129
+ case 'danger':
7130
+ // Struck through: aligned-looking glyph, cancelled. `currentColor` so the
7131
+ // glyph and the strike share the one colour declared on the wrapper.
7132
+ return /*#__PURE__*/React__default.createElement(StyledStruckThrough, null, /*#__PURE__*/React__default.createElement(Icon, _extends({
7133
+ icon: mdiFormatAlignRight,
7134
+ color: "currentColor",
7135
+ size: "small"
7136
+ }, aria)));
7137
+ default:
7138
+ return null;
7139
+ }
7140
+ }
7141
+
7142
+ /**
7143
+ * One status-list entry: its text, plus its own status icon and tooltip.
7144
+ *
7145
+ * A component rather than a plain render call because the tooltip text needs
7146
+ * translating, and `renderStructuredData` is a pure function called from the
7147
+ * adapter layer outside React — so it has no `t` to pass down. Resolving the key
7148
+ * here keeps the renderer's signature unchanged and matches how the rest of the
7149
+ * card resolves strings.
7150
+ */
7151
+ const StatusListEntry = _ref => {
7152
+ let {
7153
+ item
7154
+ } = _ref;
7155
+ const {
7156
+ t
7157
+ } = useSignalCardTranslation('investigate');
7158
+ const tooltip = item.tooltipKey ? t(item.tooltipKey) : undefined;
7159
+ // The tooltip text doubles as the icon's accessible name — see renderItemStatusIcon.
7160
+ const icon = renderItemStatusIcon(item.status, item.matchMode, tooltip);
7161
+ return /*#__PURE__*/React__default.createElement("li", null, /*#__PURE__*/React__default.createElement(StyledStatusListText, null, item.text), icon ? /*#__PURE__*/React__default.createElement(StyledStatusListIcon, null, tooltip ? /*#__PURE__*/React__default.createElement(TooltipInPortalWrapper, {
7162
+ message: tooltip
7163
+ }, /*#__PURE__*/React__default.createElement("span", {
7164
+ style: {
7165
+ display: 'inline-flex'
7166
+ }
7167
+ }, icon)) : icon) : null);
7168
+ };
7169
+
7170
+ /**
7171
+ * Render a list whose entries each carry their own status.
7172
+ *
7173
+ * Used by the DMARC card's "DKIM Signing Domain" row, where one message can
7174
+ * carry several DKIM signatures whose alignment differs — an aligned domain gets a
7175
+ * green align glyph while a non-aligned one gets a red struck-through glyph, in the
7176
+ * same list. The row-level status is cleared by signal-logic whenever per-entry
7177
+ * statuses are present, so only these icons render and nothing double-counts.
7178
+ */
7179
+ function renderStatusList(items) {
7180
+ if (!(items !== null && items !== void 0 && items.length)) {
7181
+ return null;
7182
+ }
7183
+ return /*#__PURE__*/React__default.createElement(StyledStatusList, null, items.map((item, i) => /*#__PURE__*/React__default.createElement(StatusListEntry, {
7184
+ key: i,
7185
+ item: item
7186
+ })));
7187
+ }
7188
+
6985
7189
  // =============================================================================
6986
7190
  // SIGNED HEADERS RENDERING
6987
7191
  // =============================================================================
@@ -7180,6 +7384,13 @@ function renderStructuredData(data) {
7180
7384
  }
7181
7385
  case 'list':
7182
7386
  return renderList(data.items);
7387
+
7388
+ // Additive: `list` above still ships. signal-logic falls back to it whenever
7389
+ // per-signature alignment cannot be computed (Domain Analyzer / `fromDns`
7390
+ // results, no `fromDomain`, or a domain sourced from the `dmarc.dkimDomain`
7391
+ // scalar rather than a signature), so both variants must keep working.
7392
+ case 'status-list':
7393
+ return renderStatusList(data.items);
7183
7394
  case 'highlighted-text':
7184
7395
  return /*#__PURE__*/React__default.createElement(WordHighlighter$1, {
7185
7396
  searchWord: data.searchWord,