@undrr/undrr-mangrove 2.0.0-rc.2 → 2.0.0

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.
Files changed (124) hide show
  1. package/README.md +11 -6
  2. package/components/BookCard.js +1 -1
  3. package/components/Breadcrumbs.js +2 -2
  4. package/components/Checkbox.js +1 -1
  5. package/components/Chips.js +1 -1
  6. package/components/CopyButton.js +2 -2
  7. package/components/CtaButton.js +1 -1
  8. package/components/Drawer.js +1 -1
  9. package/components/EmbedContainer.js +1 -1
  10. package/components/Footer.js +1 -1
  11. package/components/FormAction.js +1 -1
  12. package/components/FormErrorSummary.js +1 -1
  13. package/components/FormGroup.js +1 -1
  14. package/components/FullWidth.js +1 -1
  15. package/components/Gallery.js +2 -2
  16. package/components/Hero.js +2 -2
  17. package/components/HighlightBox.js +1 -1
  18. package/components/HorizontalBookCard.js +1 -1
  19. package/components/HorizontalCard.js +1 -1
  20. package/components/IconCard.js +1 -1
  21. package/components/Legend.js +1 -1
  22. package/components/Loader.js +1 -1
  23. package/components/MegaMenu.js +2 -2
  24. package/components/Notice.js +2 -2
  25. package/components/PageHeader.js +2 -2
  26. package/components/Pager.js +2 -2
  27. package/components/QuoteHighlight.js +2 -2
  28. package/components/QuoteHighlight.js.LICENSE.txt +2 -0
  29. package/components/Radio.js +1 -1
  30. package/components/Range.js +1 -1
  31. package/components/ScrollContainer.js +2 -2
  32. package/components/SegmentedControl.js +9 -0
  33. package/components/SegmentedControl.js.LICENSE.txt +9 -0
  34. package/components/Select.js +1 -1
  35. package/components/ServiceNotice.js +2 -2
  36. package/components/ShareButtons.js +2 -2
  37. package/components/ShowMore.js +2 -2
  38. package/components/SkipLink.js +1 -1
  39. package/components/StatsCard.js +1 -1
  40. package/components/SyndicationSearchWidget.js +2 -2
  41. package/components/Tab.js +2 -2
  42. package/components/TextCta.js +1 -1
  43. package/components/TextInput.js +1 -1
  44. package/components/Textarea.js +1 -1
  45. package/components/Tree.js +1 -1
  46. package/components/UserFeedback.js +1 -1
  47. package/components/VerticalCard.js +1 -1
  48. package/components/hydrate.js +2 -2
  49. package/css/style-all.css +60 -40
  50. package/css/style-all.css.map +1 -1
  51. package/css/style-delta.css +59 -39
  52. package/css/style-delta.css.map +1 -1
  53. package/css/style-gutenberg.css +16 -7
  54. package/css/style-gutenberg.css.map +1 -1
  55. package/css/style-irp.css +60 -40
  56. package/css/style-irp.css.map +1 -1
  57. package/css/style-mcr.css +59 -39
  58. package/css/style-mcr.css.map +1 -1
  59. package/css/style-preventionweb.css +60 -40
  60. package/css/style-preventionweb.css.map +1 -1
  61. package/css/style.css +59 -39
  62. package/css/style.css.map +1 -1
  63. package/error-pages/401.html +3 -3
  64. package/error-pages/403.html +3 -3
  65. package/error-pages/404.html +4 -4
  66. package/error-pages/429.html +3 -3
  67. package/error-pages/500.html +6 -6
  68. package/error-pages/502.html +3 -3
  69. package/error-pages/503.html +3 -3
  70. package/error-pages/504.html +3 -3
  71. package/error-pages/5xx.html +4 -4
  72. package/error-pages/challenge.html +3 -3
  73. package/error-pages/managed-challenge.html +3 -3
  74. package/js/README.md +31 -25
  75. package/js/copy-button.js +17 -0
  76. package/js/drawer.js +752 -0
  77. package/js/hub-header.js +217 -0
  78. package/js/show-more.js +144 -10
  79. package/js/switch-pending.js +195 -46
  80. package/package.json +8 -3
  81. package/scss/Atom/BaseTypography/Blockquote/blockquote.scss +2 -1
  82. package/scss/Atom/BaseTypography/Cite/cite.scss +2 -1
  83. package/scss/Atom/BaseTypography/Code/code.scss +8 -2
  84. package/scss/Atom/Icons/icons.scss +1 -1
  85. package/scss/Atom/Images/AuthorImage/author-image.scss +16 -1
  86. package/scss/Atom/StatusLabel/status-label.scss +331 -8
  87. package/scss/Atom/Table/table.scss +8 -3
  88. package/scss/Atom/Tag/tag.scss +20 -2
  89. package/scss/Components/Breadcrumbs/breadcrumbs.scss +8 -4
  90. package/scss/Components/Buttons/CtaButton/cta-button.scss +15 -0
  91. package/scss/Components/Cards/Card/card.scss +37 -10
  92. package/scss/Components/Cards/StatsCard/stats-card.scss +2 -10
  93. package/scss/Components/DataViz/Legend/legend.scss +2 -1
  94. package/scss/Components/ErrorPages/error-pages.scss +8 -0
  95. package/scss/Components/Forms/SegmentedControl/segmented-control.scss +412 -0
  96. package/scss/Components/Forms/_form-base.scss +109 -18
  97. package/scss/Components/Hero/hero.scss +296 -57
  98. package/scss/Components/MegaMenu/mega-menu.scss +26 -6
  99. package/scss/Components/OnThisPageNav/on-this-page-nav.scss +3 -1
  100. package/scss/Components/PageHeader/page-header.scss +13 -28
  101. package/scss/Components/Pager/pager.scss +3 -1
  102. package/scss/Components/QuoteHighlight/quote-highlight.scss +6 -1
  103. package/scss/Components/ScrollContainer/scroll-container.scss +11 -0
  104. package/scss/Components/SyndicationSearchWidget/syndication-search-widget.scss +51 -24
  105. package/scss/Components/Tab/tab.scss +1 -1
  106. package/scss/Components/TextCta/text-cta.scss +11 -3
  107. package/scss/Patterns/HubHeader/hub-header.scss +14 -3
  108. package/scss/Utilities/FullWidth/full-width.scss +14 -1
  109. package/scss/assets/scss/_components.scss +1 -0
  110. package/scss/assets/scss/_fonts.scss +11 -0
  111. package/scss/assets/scss/_foundational.scss +7 -0
  112. package/scss/assets/scss/_mixins.scss +32 -0
  113. package/scss/assets/scss/_tokens-data-viz.scss +9 -0
  114. package/scss/assets/scss/_variables.scss +16 -3
  115. package/scss/assets/scss/generated/_tokens-irp.scss +20 -4
  116. package/scss/assets/scss/generated/_tokens-mangrove.scss +6 -0
  117. package/scss/assets/scss/generated/_tokens-preventionweb.scss +20 -4
  118. package/js/__tests__/copy-button.test.js +0 -112
  119. package/js/__tests__/show-more.test.js +0 -80
  120. package/js/__tests__/table-of-contents.test.js +0 -196
  121. package/scss/Components/SyndicationSearchWidget/SyndicationSearchWidget.scss +0 -11
  122. package/scss/Components/TableOfContents/TableOfContents.scss +0 -11
  123. package/scss/Utilities/FullWidth/FullWidth.scss +0 -11
  124. package/scss/Utilities/ShowMore/ShowMore.scss +0 -11
@@ -114,6 +114,10 @@
114
114
  cursor: grabbing;
115
115
  }
116
116
 
117
+ &:focus-visible {
118
+ @include mg-focus-ring-inset;
119
+ }
120
+
117
121
  .mg-scroll--mobile & {
118
122
  cursor: default;
119
123
 
@@ -161,6 +165,13 @@
161
165
  }
162
166
 
163
167
  &--custom-width {
168
+ // A floor on the scroll box itself, which is what minWidth means. Note
169
+ // that min-width wins over the max-width: 100% above, so a value larger
170
+ // than the parent makes this box overflow its parent by design — that
171
+ // is the point of a minimum. Containing that overflow is the caller's
172
+ // job; pick a value the layout can afford, or give the parent its own
173
+ // overflow. To widen the scrollable track instead, size the items with
174
+ // itemWidth.
164
175
  min-width: var(--scroll-min-width, auto);
165
176
  }
166
177
 
@@ -26,7 +26,12 @@ $search-debug-text: var(--mg-color-yellow-dark);
26
26
 
27
27
  // Spacing & sizing (component needs rounded corners, base theme has 0)
28
28
  $search-radius: var(--mg-spacing-25);
29
- $search-input-padding-x: var(--mg-spacing-250);
29
+
30
+ // Clear button target: 36px square, clear of the WCAG 2.5.8 24x24 minimum.
31
+ $search-clear-size: mg-rem(36);
32
+
33
+ // Inline-end room for the clear button: its size plus the offset and a small gap.
34
+ $search-input-padding-x: mg-rem(44);
30
35
 
31
36
  // Screen reader only utility
32
37
  .mg-u-sr-only {
@@ -102,8 +107,8 @@ $search-input-padding-x: var(--mg-spacing-250);
102
107
  .mg-search__input {
103
108
  @extend %mg-form-input-base;
104
109
 
105
- // Override padding-right to leave room for the clear button
106
- padding-right: $search-input-padding-x;
110
+ // Override the inline-end padding to leave room for the clear button
111
+ padding-inline-end: $search-input-padding-x;
107
112
  transition:
108
113
  border-color 0.2s,
109
114
  box-shadow 0.2s;
@@ -115,18 +120,20 @@ $search-input-padding-x: var(--mg-spacing-250);
115
120
 
116
121
  .mg-search__clear {
117
122
  position: absolute;
118
- right: var(--mg-spacing-50);
123
+ inset-inline-end: var(--mg-spacing-50);
119
124
  top: 50%;
120
125
  transform: translateY(-50%);
121
126
  display: inline-flex;
122
127
  align-items: center;
123
128
  justify-content: center;
124
- width: var(--mg-spacing-200);
125
- height: var(--mg-spacing-200);
129
+ width: $search-clear-size;
130
+ height: $search-clear-size;
126
131
  padding: 0;
127
132
  background: none;
128
133
  border: none;
129
- font-size: var(--mg-font-size-100);
134
+
135
+ // Matches the icon-only button glyph size used by CTA buttons and chips.
136
+ font-size: mg-rem(18);
130
137
  color: $search-text;
131
138
  cursor: pointer;
132
139
  line-height: 1;
@@ -171,14 +178,16 @@ $search-input-padding-x: var(--mg-spacing-250);
171
178
  .mg-search__active-filters-label {
172
179
  font-weight: 500;
173
180
  color: $search-text;
174
- margin-right: var(--mg-spacing-25);
181
+ margin-inline-end: var(--mg-spacing-25);
175
182
  }
176
183
 
177
184
  .mg-search__active-filters-list {
178
185
  display: flex;
179
186
  flex-wrap: wrap;
180
187
  gap: var(--mg-spacing-50);
181
- list-style: none;
188
+
189
+ @include mg-list-unmarked;
190
+
182
191
  margin: 0;
183
192
  padding: 0;
184
193
  }
@@ -492,7 +501,7 @@ $search-input-padding-x: var(--mg-spacing-250);
492
501
  .mg-search__result-date {
493
502
  &::before {
494
503
  content: "•";
495
- margin-right: var(--mg-spacing-50);
504
+ margin-inline-end: var(--mg-spacing-50);
496
505
  color: $search-border;
497
506
  }
498
507
  }
@@ -949,7 +958,7 @@ $search-input-padding-x: var(--mg-spacing-250);
949
958
  padding: var(--mg-spacing-50) var(--mg-spacing-75);
950
959
  font-size: var(--mg-font-size-300);
951
960
  color: $search-text;
952
- text-align: left;
961
+ text-align: start;
953
962
  background-color: rgb(var(--mg-color-neutral-50));
954
963
  border: 1px solid rgb(var(--mg-color-neutral-600) / 0.22);
955
964
  border-radius: var(--mg-radius-form-input);
@@ -986,7 +995,7 @@ $search-input-padding-x: var(--mg-spacing-250);
986
995
 
987
996
  .mg-select__trigger-icon {
988
997
  flex-shrink: 0;
989
- margin-left: var(--mg-spacing-50);
998
+ margin-inline-start: var(--mg-spacing-50);
990
999
  color: $search-text-muted;
991
1000
  transition: transform 0.2s ease;
992
1001
 
@@ -1148,7 +1157,7 @@ $search-input-padding-x: var(--mg-spacing-250);
1148
1157
 
1149
1158
  // Subtype options (indented children under parent content types)
1150
1159
  .mg-select__option--subtype {
1151
- padding-left: var(--mg-spacing-150);
1160
+ padding-inline-start: var(--mg-spacing-150);
1152
1161
 
1153
1162
  .mg-select__option-label {
1154
1163
  font-size: var(--mg-font-size-100);
@@ -1156,7 +1165,7 @@ $search-input-padding-x: var(--mg-spacing-250);
1156
1165
  // Add visual connector
1157
1166
  &::before {
1158
1167
  content: "└";
1159
- margin-right: var(--mg-spacing-25);
1168
+ margin-inline-end: var(--mg-spacing-25);
1160
1169
  color: $search-text-muted;
1161
1170
  }
1162
1171
  }
@@ -1194,7 +1203,7 @@ $search-input-padding-x: var(--mg-spacing-250);
1194
1203
  }
1195
1204
 
1196
1205
  label {
1197
- padding-left: var(--mg-spacing-25);
1206
+ padding-inline-start: var(--mg-spacing-25);
1198
1207
  font-weight: normal;
1199
1208
  text-wrap: balance;
1200
1209
  cursor: pointer;
@@ -1226,7 +1235,7 @@ $search-input-padding-x: var(--mg-spacing-250);
1226
1235
  }
1227
1236
 
1228
1237
  // ========================================
1229
- // Mobile Filter Drawer (Left Slide-in)
1238
+ // Mobile Filter Drawer (Inline-start Slide-in)
1230
1239
  // ========================================
1231
1240
  // UX patterns informed by:
1232
1241
  // @see https://www.pencilandpaper.io/articles/ux-pattern-analysis-mobile-filters
@@ -1254,24 +1263,42 @@ $search-input-padding-x: var(--mg-spacing-250);
1254
1263
  }
1255
1264
  }
1256
1265
 
1266
+ // The panel, the direction it travels and the side its shadow falls on are
1267
+ // all driven by two custom properties flipped under `:dir(rtl)`, the same
1268
+ // shape `.mg-drawer--start` uses in Navigation/Drawer. `inset-inline-start`
1269
+ // on its own would fight the keyframe, which translates on a physical axis:
1270
+ // the resting edge would move to the right in Arabic while the panel still
1271
+ // slid in from the left.
1257
1272
  .mg-search__drawer {
1273
+ --mg-search-drawer-offset: -100%;
1274
+ --mg-search-drawer-shadow-offset: 4px;
1275
+
1258
1276
  position: fixed;
1259
- top: 0;
1260
- left: 0;
1261
- bottom: 0;
1277
+ inset-block: 0;
1278
+ inset-inline-start: 0;
1262
1279
  width: min(320px, 85vw); // Max 320px or 85% of viewport
1263
1280
  background: #fff;
1264
- box-shadow: 4px 0 20px rgb(0 0 0 / 0.15);
1281
+ box-shadow: var(--mg-search-drawer-shadow-offset) 0 20px rgb(0 0 0 / 0.15);
1265
1282
  display: flex;
1266
1283
  flex-direction: column;
1267
- animation: mg-search-slide-in-left 0.3s ease-out;
1284
+ animation: mg-search-slide-in-start 0.3s ease-out;
1268
1285
  z-index: var(--mg-z-index-drawer);
1269
1286
  cursor: default; // Reset cursor inside drawer
1287
+
1288
+ // The shadow points at the page content, so it mirrors with the panel.
1289
+ &:dir(rtl) {
1290
+ --mg-search-drawer-offset: 100%;
1291
+ --mg-search-drawer-shadow-offset: -4px;
1292
+ }
1293
+
1294
+ @media (prefers-reduced-motion: reduce) {
1295
+ animation: none;
1296
+ }
1270
1297
  }
1271
1298
 
1272
- @keyframes mg-search-slide-in-left {
1299
+ @keyframes mg-search-slide-in-start {
1273
1300
  from {
1274
- transform: translateX(-100%);
1301
+ transform: translateX(var(--mg-search-drawer-offset));
1275
1302
  }
1276
1303
  to {
1277
1304
  transform: translateX(0);
@@ -1518,7 +1545,7 @@ $search-input-padding-x: var(--mg-spacing-250);
1518
1545
  .mg-search__filter-chip-connector {
1519
1546
  display: inline-flex;
1520
1547
  align-items: center;
1521
- margin-right: var(--mg-spacing-25);
1548
+ margin-inline-end: var(--mg-spacing-25);
1522
1549
  color: $search-text-muted;
1523
1550
  font-weight: 600;
1524
1551
  text-transform: uppercase;
@@ -89,7 +89,7 @@
89
89
  > .mg-tabs__mobile-item
90
90
  > .mg-tabs__mobile-link {
91
91
  position: relative;
92
- padding-right: mg-rem(40); // space for the chevron to theright
92
+ padding-right: mg-rem(40); // space for the chevron to the right
93
93
 
94
94
  &:hover,
95
95
  &:focus-visible {
@@ -147,10 +147,16 @@
147
147
  background-color: rgb(var(--mg-color-hero));
148
148
  }
149
149
 
150
+ // The orange accent carries no text at any size, so this variant no longer
151
+ // paints it flat under an eyebrow, a headline and a 90%-white body. It takes
152
+ // the soft treatment below — a white surface under an 8% wash of the accent,
153
+ // with dark copy — and keeps the accent as a solid leading rule, which no
154
+ // copy sits on. See
155
+ // docs/COLOUR-CONTRAST-METHODOLOGY.md#the-orange-accent-carries-no-text
150
156
  &--secondary {
151
157
  --mg-cta-accent: var(--mg-color-hero--secondary);
152
158
 
153
- background-color: rgb(var(--mg-color-hero--secondary));
159
+ border-inline-start: mg-rem(6) solid rgb(var(--mg-cta-accent));
154
160
  }
155
161
 
156
162
  &--tertiary {
@@ -165,7 +171,8 @@
165
171
  background-color: rgb(var(--mg-color-hero--quaternary));
166
172
  }
167
173
 
168
- &--soft {
174
+ &--soft,
175
+ &--secondary {
169
176
  background-color: rgb(var(--mg-color-neutral-0));
170
177
  background-image: linear-gradient(
171
178
  rgb(var(--mg-cta-accent) / 0.08),
@@ -187,7 +194,8 @@
187
194
  }
188
195
 
189
196
  // Strong surfaces need inverse buttons; soft surfaces use normal buttons.
190
- &:where(:not(.mg-cta--soft)) {
197
+ // --secondary is a soft surface too, since the orange accent left its fill.
198
+ &:where(:not(.mg-cta--soft, .mg-cta--secondary)) {
191
199
  // Light tinted stroke keeps filled buttons visible on the strong surface.
192
200
  --mg-border-color-button-primary: rgb(var(--mg-color-neutral-0) / 0.72);
193
201
  --mg-border-color-button-secondary: rgb(var(--mg-color-neutral-0) / 0.72);
@@ -41,9 +41,18 @@
41
41
  --mg-hub-header-bar-tint: 0 0 0;
42
42
  --mg-hub-header-bar-tint-alpha: 0.14;
43
43
 
44
+ // hero--secondary is the orange accent, which carries no text at any size —
45
+ // every link and heading here was white on it at 2.65, and the bar tint
46
+ // darkened it nowhere near enough. So the surface stays the theme's own hero
47
+ // colour, which the white ink above is already sized for, and the accent
48
+ // becomes the rule the bar sits on: a line, with no copy over it.
49
+ // See docs/COLOUR-CONTRAST-METHODOLOGY.md#the-orange-accent-carries-no-text
44
50
  &--surface-secondary {
45
- --mg-hub-header-surface: var(--mg-color-hero--secondary);
46
- --mg-hub-header-banner-surface: var(--mg-color-hero--secondary);
51
+ // Full-bleed, because the bar it sits under is full-bleed, so the rule
52
+ // reads as the edge of the header rather than a line inside it.
53
+ .mg-hub-header__bar {
54
+ border-block-end: mg-rem(4) solid rgb(var(--mg-color-hero--secondary));
55
+ }
47
56
  }
48
57
 
49
58
  &--surface-tertiary {
@@ -233,7 +242,9 @@
233
242
  overflow-x: auto;
234
243
  scrollbar-width: none;
235
244
  scroll-padding-inline: var(--mg-spacing-150);
236
- list-style: none;
245
+
246
+ @include mg-list-unmarked;
247
+
237
248
  padding: 0;
238
249
  margin: 0;
239
250
  gap: var(--mg-spacing-50);
@@ -35,6 +35,19 @@
35
35
  z-index: var(--mg-z-index-behind); // behind siblings, not behind ancestors
36
36
  }
37
37
 
38
+ // RTL keeps the physical override rather than a logical `margin-inline-start`:
39
+ // the pseudo-element is absolutely positioned with both offsets `auto`, so it
40
+ // hangs off its static position, and a flex or grid parent resolves that static
41
+ // position from the inline-end edge in RTL. Reset the inherited `margin-left`
42
+ // here, or both negative margins apply at once and the bleed lands ~13px off
43
+ // centre inside a flex parent such as `mg-mega-topbar`, leaving an unpainted
44
+ // strip at the inline-end edge.
45
+ // `left: 50%` is not a physical property left un-mirrored: the mirrored form
46
+ // (`right: 50%` with `margin-right: -50vw`) was measured and reintroduces the
47
+ // offset this anchor exists to remove. Do not tidy it.
48
+ // See unisdr/undrr-mangrove#1203.
38
49
  [dir="rtl"] .mg-container-full-width::before {
39
- margin-right: calc(50% - 50vw);
50
+ left: 50%;
51
+ margin-left: -50vw;
52
+ margin-right: 0;
40
53
  }
@@ -44,6 +44,7 @@
44
44
  @import "../../Components/Forms/Textarea/textarea";
45
45
  @import "../../Components/Forms/Checkbox/checkbox";
46
46
  @import "../../Components/Forms/Radio/radio";
47
+ @import "../../Components/Forms/SegmentedControl/segmented-control";
47
48
  @import "../../Components/Forms/Select/select";
48
49
  @import "../../Components/Forms/Range/range";
49
50
  @import "../../Components/Breadcrumbs/breadcrumbs";
@@ -170,3 +170,14 @@ $font-path-noto-sans-arabic: "https://assets.undrr.org/fonts/noto-sans-arabic/v1
170
170
 
171
171
  font-family: var(--mg-font-family-text);
172
172
  }
173
+
174
+ // Arabic joins letters: Latin tracking must never separate or overlap them.
175
+ // Match the element's inherited language, not an Arabic ancestor, so explicit
176
+ // non-Arabic islands keep their own tracking. Important is intentional: this
177
+ // script constraint must beat later component rules and consumer import order.
178
+ // Include generated labels (for example pre[data-language]::after).
179
+ :lang(ar),
180
+ :lang(ar)::before,
181
+ :lang(ar)::after {
182
+ letter-spacing: normal !important;
183
+ }
@@ -188,6 +188,13 @@ a {
188
188
  }
189
189
  }
190
190
 
191
+ // Long-form and WYSIWYG content opt into this boundary. Navigation, cards and
192
+ // button-like links carry their purpose through their component treatment;
193
+ // prose links do not, so colour alone is insufficient (WCAG 1.4.1).
194
+ .mg-content p a {
195
+ text-decoration: underline;
196
+ }
197
+
191
198
  @media (prefers-reduced-motion: reduce) {
192
199
  *,
193
200
  *::before,
@@ -148,6 +148,38 @@
148
148
  }
149
149
  }
150
150
 
151
+ // Unmarked list — hide a <ul>/<ol>'s markers without losing its list role.
152
+
153
+ // Safari removes the implicit `list` role from any list whose `list-style` is
154
+ // `none`, so VoiceOver stops announcing "list, N items" for every navigation,
155
+ // pager, legend and menu we style that way (unisdr/undrr-mangrove#1209). An
156
+ // empty string IS a marker, so the role survives, and it renders nothing and
157
+ // reserves no space.
158
+
159
+ // Order matters. The shorthand goes first as the fallback: a browser too old
160
+ // to parse a string `list-style-type` drops that declaration and keeps `none`,
161
+ // behaving exactly as it did before. Never emit `list-style: none` on its own.
162
+
163
+ // This is deliberately a CSS fix rather than `role="list"` in the markup.
164
+ // Consumers hand-write these lists in Drupal templates and static HTML, so
165
+ // only a fix on the class reaches all of them — and `jsx-a11y` flags the
166
+ // redundant role in our own examples. See the "List semantics" rule in
167
+ // docs/REVIEW-CHECKLIST.md.
168
+
169
+ // It belongs on the list items too, wherever an `<li>` rule hides its own
170
+ // marker. `list-style-type` is inherited, so the list's empty string reaches
171
+ // the items — but an item rule setting `none` wins over that inheritance, and
172
+ // it is the item's marker Safari inspects. A bare `list-style: none` on an
173
+ // `<li>` silently undoes the fix on its list.
174
+
175
+ // Do NOT use this on a list that carries a different ARIA role on purpose
176
+ // (`tree`, `group`, `menu`, `listbox`, `tablist`): that role already replaces
177
+ // list semantics, and there is nothing to preserve.
178
+ @mixin mg-list-unmarked {
179
+ list-style: none;
180
+ list-style-type: "";
181
+ }
182
+
151
183
  /* mixing end */
152
184
 
153
185
  /* placeholder start */
@@ -42,6 +42,15 @@
42
42
  * N=2 ΔE 56.8 N=3 ΔE 19.5 N=4 ΔE 19.5
43
43
  * N=5 ΔE 11.1 N=6 ΔE 8.3 N=7 ΔE 3.2
44
44
  *
45
+ * The worst pair itself, so a slot change can be checked against the right
46
+ * two colours: N=2 is 1&2 (protan), N=3 and N=4 are 1&3 (deutan), N=5 is 1&5
47
+ * (protan), N=6 is 2&6 (protan), N=7 is 1&7 (protan).
48
+ *
49
+ * METHOD — reproduce these before changing a slot; the steps are not
50
+ * interchangeable and the wrong order gives wrong answers. See
51
+ * docs/COLOUR-CONTRAST-METHODOLOGY.md, "How colour-vision separation is
52
+ * measured", for the full procedure and why each step matters.
53
+ *
45
54
  * Read that as: up to 4 series, colour alone is sufficient. At 5 it is
46
55
  * marginal. At 6 or 7 colour alone is NOT sufficient — slots 1 and 7 are
47
56
  * near-identical to a protanope (ΔE 3.2) — so a chart using more than five
@@ -4,7 +4,7 @@
4
4
  * Storybook: https://mangrove.undrr.org/
5
5
  * LLMs context: https://mangrove.undrr.org/llms.txt
6
6
  * Repository: https://github.com/unisdr/undrr-mangrove
7
- * Version: 2.0.0-rc.2
7
+ * Version: 2.0.0
8
8
  *
9
9
  * ██╗ ██╗███╗ ██╗██████╗ ██████╗ ██████╗
10
10
  * ██║ ██║████╗ ██║██╔══██╗██╔══██╗██╔══██╗
@@ -303,8 +303,21 @@ $mg-tabs-border-bottom: true !default;
303
303
  // The dots are decoration, not the message — the label text always names the
304
304
  // status, so SC 1.4.1 does not depend on them. They still need a boundary:
305
305
  // the pale swatches sit at 1.3-2.2:1 against a white page, so a neutral-500
306
- // ring (5.74:1 on white) gives every indicator a visible edge. Set
307
- // --mg-status-label-indicator-border-width to 0 to drop it.
306
+ // ring (5.74:1 on white) gives every indicator a visible edge — the same
307
+ // width on every edge of all seven shapes, including the two drawn with a
308
+ // clip path (unisdr/undrr-mangrove#1208). Set
309
+ // --mg-status-label-indicator-border-width to 0 to drop it, bearing in mind
310
+ // that the ring is the only part of the four pale marks clearing 3:1, and
311
+ // that in forced colours it is also what keeps the base indicator visible.
312
+ // -
313
+ // No theme overrides any of the seven fills, so all five brands render the
314
+ // same status palette. That is deliberate: these are DELTA's editorial
315
+ // workflow colours and the shared service-health pair, not brand identity,
316
+ // and a status that changed colour per site would stop being one status.
317
+ // Do not rebalance the fills for colour-vision separation without reading
318
+ // StatusLabel.mdx, "Why the palette is not rebalanced instead" — it is
319
+ // measured, and the alternatives are worse on contrast, on normal vision, or
320
+ // on both.
308
321
  --mg-status-label-gap: var(--mg-spacing-75);
309
322
  --mg-status-label-font-size: var(--mg-font-size-300);
310
323
  --mg-status-label-color: rgb(var(--mg-color-text));
@@ -31,11 +31,21 @@
31
31
  --mg-color-button-background: rgb(15 120 191);
32
32
 
33
33
  // color.button-background--hover
34
- --mg-color-button-background--hover: rgb(var(--mg-color-orange-900));
34
+ // Deeper partner of the IRP interactive blue. Carries the secondary button
35
+ // fill and, since the primary button's hover left the orange accent, that
36
+ // hover too. White on it is 6.02:1 and 71.1 perceptually, so it clears AA
37
+ // either way, and it sits 6.6 dE2000 from the interactive blue —
38
+ // comfortably past the ~2.3 just-noticeable difference, and of the same
39
+ // order as the base theme's own blue-900 to blue-700 hover (12.2).
40
+ --mg-color-button-background--hover: rgb(13 103 163);
35
41
 
36
42
  // color.button-secondary-background
37
- // Deeper partner of the IRP interactive blue for secondary button fill,
38
- // meeting WCAG AA (6.0:1).
43
+ // Deeper partner of the IRP interactive blue. Carries the secondary button
44
+ // fill and, since the primary button's hover left the orange accent, that
45
+ // hover too. White on it is 6.02:1 and 71.1 perceptually, so it clears AA
46
+ // either way, and it sits 6.6 dE2000 from the interactive blue —
47
+ // comfortably past the ~2.3 just-noticeable difference, and of the same
48
+ // order as the base theme's own blue-900 to blue-700 hover (12.2).
39
49
  --mg-color-button-secondary-background: 13 103 163;
40
50
 
41
51
  // color.button-secondary-background--hover
@@ -46,7 +56,13 @@
46
56
  --mg-color-button-outline-primary: 15 120 191;
47
57
 
48
58
  // color.button-outline-primary--hover
49
- --mg-color-button-outline-primary--hover: var(--mg-color-orange-900);
59
+ // Deeper partner of the IRP interactive blue. Carries the secondary button
60
+ // fill and, since the primary button's hover left the orange accent, that
61
+ // hover too. White on it is 6.02:1 and 71.1 perceptually, so it clears AA
62
+ // either way, and it sits 6.6 dE2000 from the interactive blue —
63
+ // comfortably past the ~2.3 just-noticeable difference, and of the same
64
+ // order as the base theme's own blue-900 to blue-700 hover (12.2).
65
+ --mg-color-button-outline-primary--hover: 13 103 163;
50
66
 
51
67
  // color.button-outline-secondary
52
68
  --mg-color-button-outline-secondary: var(--mg-color-button-secondary-background);
@@ -585,6 +585,12 @@ $mg-font-line-height-700: 1.5em;
585
585
  // hero-gradient.end-opacity
586
586
  --mg-hero-gradient-end-opacity: 0.08;
587
587
 
588
+ // hero-scrim.opacity
589
+ --mg-hero-scrim-opacity: 0.16;
590
+
591
+ // hero-scrim.fade
592
+ --mg-hero-scrim-fade: 10rem;
593
+
588
594
  // z-index.behind
589
595
  --mg-z-index-behind: -1;
590
596
 
@@ -31,11 +31,21 @@
31
31
  --mg-color-button-background: rgb(10 105 105);
32
32
 
33
33
  // color.button-background--hover
34
- --mg-color-button-background--hover: rgb(var(--mg-color-orange-900));
34
+ // Lighter partner of the brand teal. Carries the secondary button fill and,
35
+ // since the primary button's hover left the orange accent, that hover too.
36
+ // White on it is 5.5:1 and 69.7 perceptually, so it clears AA either way,
37
+ // and it sits 4.2 dE2000 from the brand teal — comfortably past the ~2.3
38
+ // just-noticeable difference, though a shallower step than the base theme's
39
+ // own blue-900 to blue-700 hover (12.2).
40
+ --mg-color-button-background--hover: rgb(12 117 117);
35
41
 
36
42
  // color.button-secondary-background
37
- // Lighter partner of the brand teal for secondary button fill, meeting WCAG
38
- // AA (5.5:1) while providing lower visual weight than primary.
43
+ // Lighter partner of the brand teal. Carries the secondary button fill and,
44
+ // since the primary button's hover left the orange accent, that hover too.
45
+ // White on it is 5.5:1 and 69.7 perceptually, so it clears AA either way,
46
+ // and it sits 4.2 dE2000 from the brand teal — comfortably past the ~2.3
47
+ // just-noticeable difference, though a shallower step than the base theme's
48
+ // own blue-900 to blue-700 hover (12.2).
39
49
  --mg-color-button-secondary-background: 12 117 117;
40
50
 
41
51
  // color.button-secondary-background--hover
@@ -46,7 +56,13 @@
46
56
  --mg-color-button-outline-primary: 10 105 105;
47
57
 
48
58
  // color.button-outline-primary--hover
49
- --mg-color-button-outline-primary--hover: var(--mg-color-orange-900);
59
+ // Lighter partner of the brand teal. Carries the secondary button fill and,
60
+ // since the primary button's hover left the orange accent, that hover too.
61
+ // White on it is 5.5:1 and 69.7 perceptually, so it clears AA either way,
62
+ // and it sits 4.2 dE2000 from the brand teal — comfortably past the ~2.3
63
+ // just-noticeable difference, though a shallower step than the base theme's
64
+ // own blue-900 to blue-700 hover (12.2).
65
+ --mg-color-button-outline-primary--hover: 12 117 117;
50
66
 
51
67
  // color.button-outline-secondary
52
68
  --mg-color-button-outline-secondary: var(--mg-color-button-secondary-background);
@@ -1,112 +0,0 @@
1
- import { mgCopyButton } from '../copy-button';
2
-
3
- describe('mgCopyButton (Vanilla JS Script)', () => {
4
- let writeTextMock;
5
-
6
- beforeEach(() => {
7
- document.body.innerHTML = '';
8
- jest.useFakeTimers();
9
-
10
- writeTextMock = jest.fn().mockResolvedValue(undefined);
11
- Object.assign(navigator, {
12
- clipboard: {
13
- writeText: writeTextMock,
14
- },
15
- });
16
- });
17
-
18
- afterEach(() => {
19
- jest.useRealTimers();
20
- jest.restoreAllMocks();
21
- });
22
-
23
- function createButton(attrs = {}) {
24
- const btn = document.createElement('button');
25
- btn.type = 'button';
26
- btn.className =
27
- 'mg-button mg-button-primary mg-button-outline mg-button--icon mg-copy-button';
28
- btn.setAttribute('data-mg-copy-button', '');
29
- Object.entries(attrs).forEach(([key, val]) => {
30
- btn.setAttribute(`data-${key}`, val);
31
- });
32
-
33
- btn.innerHTML = `
34
- <span class="mg-icon mg-icon-copy mg-button__icon" aria-hidden="true"></span>
35
- <span class="mg-copy-button__feedback" role="status" aria-hidden="true">Copied!</span>
36
- <span class="mg-u-sr-only" aria-live="polite"></span>
37
- `;
38
-
39
- document.body.appendChild(btn);
40
- return btn;
41
- }
42
-
43
- it('initializes buttons and writes to clipboard on click', async () => {
44
- const btn = createButton({
45
- 'text-to-copy': 'https://preventionweb.net',
46
- 'tooltip-label': 'Copied!',
47
- 'copied-label': 'URL copied to clipboard.',
48
- });
49
-
50
- mgCopyButton(btn);
51
-
52
- // Trigger click
53
- btn.click();
54
- await Promise.resolve();
55
-
56
- expect(writeTextMock).toHaveBeenCalledWith('https://preventionweb.net');
57
- expect(btn.classList.contains('mg-copy-button--copied')).toBe(true);
58
-
59
- const feedback = btn.querySelector('.mg-copy-button__feedback');
60
- expect(
61
- feedback.classList.contains('mg-copy-button__feedback--visible')
62
- ).toBe(true);
63
- expect(feedback.textContent).toBe('Copied!');
64
-
65
- const liveRegion = btn.querySelector('.mg-u-sr-only');
66
- expect(liveRegion.textContent).toBe('URL copied to clipboard.');
67
-
68
- // Fast-forward 2 seconds
69
- jest.advanceTimersByTime(2000);
70
-
71
- expect(btn.classList.contains('mg-copy-button--copied')).toBe(false);
72
- expect(
73
- feedback.classList.contains('mg-copy-button__feedback--visible')
74
- ).toBe(false);
75
- expect(liveRegion.textContent).toBe('');
76
- });
77
-
78
- it('guards against duplicate initialization', () => {
79
- const btn = createButton({ 'text-to-copy': 'Single' });
80
-
81
- mgCopyButton(btn);
82
- expect(btn.dataset.mgCopyButtonInitialized).toBe('true');
83
-
84
- // Second call does not double-bind
85
- mgCopyButton(btn);
86
- expect(btn.dataset.mgCopyButtonInitialized).toBe('true');
87
- });
88
-
89
- it('supports initializing via container elements or document', async () => {
90
- const container = document.createElement('div');
91
- container.innerHTML = `
92
- <div class="card">
93
- <button type="button" class="mg-button mg-copy-button" data-mg-copy-button data-text-to-copy="https://nested.org">
94
- <span class="mg-icon mg-icon-copy mg-button__icon"></span>
95
- <span class="mg-copy-button__feedback">Copied!</span>
96
- <span class="mg-u-sr-only"></span>
97
- </button>
98
- </div>
99
- `;
100
- document.body.appendChild(container);
101
-
102
- mgCopyButton(container);
103
-
104
- const btn = container.querySelector('[data-mg-copy-button]');
105
- expect(btn.dataset.mgCopyButtonInitialized).toBe('true');
106
-
107
- btn.click();
108
- await Promise.resolve();
109
-
110
- expect(writeTextMock).toHaveBeenCalledWith('https://nested.org');
111
- });
112
- });