@undrr/undrr-mangrove 2.0.0 → 2.1.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 (182) hide show
  1. package/README.md +9 -5
  2. package/components/BookCard.js +2 -2
  3. package/components/Breadcrumbs.js +2 -2
  4. package/components/Checkbox.js +2 -2
  5. package/components/Chips.js +2 -2
  6. package/components/CopyButton.js +2 -2
  7. package/components/CtaButton.js +2 -2
  8. package/components/Drawer.js +3 -3
  9. package/components/Drawer.js.LICENSE.txt +3 -1
  10. package/components/EmbedContainer.js +2 -2
  11. package/components/Footer.js +2 -2
  12. package/components/FormAction.js +2 -2
  13. package/components/FormErrorSummary.js +2 -2
  14. package/components/FormGroup.js +2 -2
  15. package/components/FullWidth.js +2 -2
  16. package/components/Gallery.js +3 -3
  17. package/components/Hero.js +3 -3
  18. package/components/HighlightBox.js +2 -2
  19. package/components/HorizontalBookCard.js +3 -3
  20. package/components/HorizontalBookCard.js.LICENSE.txt +3 -1
  21. package/components/HorizontalCard.js +3 -3
  22. package/components/HorizontalCard.js.LICENSE.txt +3 -1
  23. package/components/IconCard.js +3 -3
  24. package/components/IconCard.js.LICENSE.txt +3 -1
  25. package/components/Legend.js +2 -2
  26. package/components/Loader.js +3 -3
  27. package/components/MegaMenu.js +3 -3
  28. package/components/Notice.js +2 -2
  29. package/components/PageHeader.js +2 -2
  30. package/components/Pager.js +2 -2
  31. package/components/ProgressBar.js +9 -0
  32. package/components/ProgressBar.js.LICENSE.txt +9 -0
  33. package/components/QuoteHighlight.js +3 -3
  34. package/components/QuoteHighlight.js.LICENSE.txt +3 -1
  35. package/components/Radio.js +2 -2
  36. package/components/Range.js +2 -2
  37. package/components/ScrollContainer.js +2 -2
  38. package/components/SegmentedControl.js +2 -2
  39. package/components/Select.js +2 -2
  40. package/components/ServiceNotice.js +2 -2
  41. package/components/ShareButtons.js +3 -3
  42. package/components/ShowMore.js +3 -3
  43. package/components/SkipLink.js +2 -2
  44. package/components/StatsCard.js +3 -3
  45. package/components/StatsCard.js.LICENSE.txt +3 -1
  46. package/components/SyndicationSearchWidget.js +3 -3
  47. package/components/Tab.js +2 -2
  48. package/components/TextCta.js +3 -3
  49. package/components/TextCta.js.LICENSE.txt +3 -1
  50. package/components/TextInput.js +2 -2
  51. package/components/Textarea.js +2 -2
  52. package/components/Tree.js +2 -2
  53. package/components/UserFeedback.js +2 -2
  54. package/components/VerticalCard.js +3 -3
  55. package/components/VerticalCard.js.LICENSE.txt +3 -1
  56. package/components/hydrate.js +3 -3
  57. package/css/style-all.css +147 -128
  58. package/css/style-all.css.map +1 -1
  59. package/css/style-delta.css +147 -128
  60. package/css/style-delta.css.map +1 -1
  61. package/css/style-gutenberg.css +12 -12
  62. package/css/style-gutenberg.css.map +1 -1
  63. package/css/style-irp.css +147 -128
  64. package/css/style-irp.css.map +1 -1
  65. package/css/style-mcr.css +147 -128
  66. package/css/style-mcr.css.map +1 -1
  67. package/css/style-preventionweb.css +147 -128
  68. package/css/style-preventionweb.css.map +1 -1
  69. package/css/style.css +147 -128
  70. package/css/style.css.map +1 -1
  71. package/docs/EDITORIAL-MANUAL.md +193 -0
  72. package/docs/llms-editorial-manual.txt +200 -0
  73. package/docs/llms-motion.txt +125 -0
  74. package/docs/llms-page-building.txt +1140 -0
  75. package/docs/llms-search-widget.txt +765 -0
  76. package/error-pages/401.html +1 -1
  77. package/error-pages/403.html +1 -1
  78. package/error-pages/404.html +1 -1
  79. package/error-pages/429.html +1 -1
  80. package/error-pages/500.html +1 -1
  81. package/error-pages/502.html +1 -1
  82. package/error-pages/503.html +1 -1
  83. package/error-pages/504.html +1 -1
  84. package/error-pages/5xx.html +1 -1
  85. package/error-pages/challenge.html +1 -1
  86. package/error-pages/managed-challenge.html +1 -1
  87. package/js/cite-this.js +994 -0
  88. package/js/on-this-page-nav.js +80 -1
  89. package/js/show-more.js +149 -21
  90. package/package.json +6 -1
  91. package/scss/Atom/BaseTypography/Abbr/abbr.scss +1 -1
  92. package/scss/Atom/BaseTypography/Blockquote/blockquote.scss +1 -1
  93. package/scss/Atom/BaseTypography/Cite/cite.scss +1 -1
  94. package/scss/Atom/BaseTypography/Code/code.scss +1 -1
  95. package/scss/Atom/BaseTypography/Mark/mark.scss +1 -1
  96. package/scss/Atom/BaseTypography/Quotation/quotation.scss +1 -1
  97. package/scss/Atom/BaseTypography/Small/small.scss +1 -1
  98. package/scss/Atom/Colors/color.scss +1 -1
  99. package/scss/Atom/EmptyState/empty-state.scss +1 -1
  100. package/scss/Atom/Icons/_icon-definitions.scss +35 -3
  101. package/scss/Atom/Icons/icons.scss +1 -1
  102. package/scss/Atom/Images/AuthorImage/author-image.scss +10 -3
  103. package/scss/Atom/Images/Image/image.scss +1 -1
  104. package/scss/Atom/Images/ImageCaptionCredit/image-caption-credit.scss +7 -2
  105. package/scss/Atom/Images/ImageCredit/image-credit.scss +1 -1
  106. package/scss/Atom/Layout/Container/container.scss +1 -1
  107. package/scss/Atom/Layout/Grid/grid.scss +1 -1
  108. package/scss/Atom/Layout/Spacing/spacing.scss +1 -1
  109. package/scss/Atom/Logo/logo.scss +1 -1
  110. package/scss/Atom/Navigation/Breadcrumb/breadcrumb.scss +1 -1
  111. package/scss/Atom/Navigation/MenuItems/menu-items.scss +1 -1
  112. package/scss/Atom/ReachElement/Details/details.scss +5 -3
  113. package/scss/Atom/ReachElement/Figcaption/figcaption.scss +1 -1
  114. package/scss/Atom/StatusLabel/status-label.scss +1 -1
  115. package/scss/Atom/Table/table.scss +1 -1
  116. package/scss/Atom/Tag/tag.scss +1 -1
  117. package/scss/Atom/Video/video.scss +1 -1
  118. package/scss/Components/Boilerplate/boilerplate.scss +1 -1
  119. package/scss/Components/Breadcrumbs/breadcrumbs.scss +1 -1
  120. package/scss/Components/Buttons/Chips/chips.scss +1 -1
  121. package/scss/Components/Buttons/CopyButton/copy-button.scss +1 -1
  122. package/scss/Components/Buttons/CtaButton/buttons.scss +1 -1
  123. package/scss/Components/Buttons/CtaButton/cta-button.scss +1 -1
  124. package/scss/Components/Buttons/ShareButtons/share-buttons.scss +90 -1
  125. package/scss/Components/Cards/Card/card.scss +9 -3
  126. package/scss/Components/Cards/StatsCard/stats-card.scss +9 -2
  127. package/scss/Components/CiteThis/cite-this.scss +339 -0
  128. package/scss/Components/CookieConsentBanner/cookie-consent-banner.scss +1 -1
  129. package/scss/Components/DataViz/Legend/legend.scss +1 -1
  130. package/scss/Components/ErrorPages/error-pages.scss +1 -1
  131. package/scss/Components/Footer/footer.scss +1 -1
  132. package/scss/Components/Forms/Checkbox/checkbox.scss +1 -1
  133. package/scss/Components/Forms/FormAction/form-action.scss +1 -1
  134. package/scss/Components/Forms/FormGroup/form-group.scss +1 -1
  135. package/scss/Components/Forms/InputFields/input-fields.scss +1 -1
  136. package/scss/Components/Forms/Radio/radio.scss +1 -1
  137. package/scss/Components/Forms/Range/range.scss +1 -1
  138. package/scss/Components/Forms/SegmentedControl/segmented-control.scss +1 -1
  139. package/scss/Components/Forms/Select/select.scss +1 -1
  140. package/scss/Components/Forms/TextInput/text-input.scss +1 -1
  141. package/scss/Components/Forms/Textarea/textarea.scss +1 -1
  142. package/scss/Components/Forms/_form-base.scss +1 -1
  143. package/scss/Components/Gallery/gallery.scss +41 -16
  144. package/scss/Components/Hero/hero.scss +166 -1
  145. package/scss/Components/HighlightBox/highlight-box.scss +1 -1
  146. package/scss/Components/MegaMenu/mega-menu.scss +43 -122
  147. package/scss/Components/MegaMenu/megamenu.scss +1 -1
  148. package/scss/Components/Navigation/Drawer/drawer.scss +11 -1
  149. package/scss/Components/Navigation/Tree/tree.scss +1 -1
  150. package/scss/Components/Notice/notice.scss +1 -1
  151. package/scss/Components/OnThisPageNav/on-this-page-nav.scss +109 -4
  152. package/scss/Components/PageHeader/page-header.scss +1 -1
  153. package/scss/Components/Pager/pager.scss +1 -1
  154. package/scss/Components/PreviewAccess/preview-access.scss +1 -1
  155. package/scss/Components/ProgressBar/progress-bar.scss +215 -0
  156. package/scss/Components/QuoteHighlight/quote-highlight.scss +1 -1
  157. package/scss/Components/ScrollContainer/scroll-container.scss +1 -1
  158. package/scss/Components/Snackbar/snackbar.scss +1 -1
  159. package/scss/Components/SyndicationSearchWidget/_deprecated-teaser-hide.scss +1 -1
  160. package/scss/Components/SyndicationSearchWidget/syndication-search-widget.scss +253 -128
  161. package/scss/Components/Tab/tab.scss +3 -2
  162. package/scss/Components/TableOfContents/table-of-contents.scss +1 -1
  163. package/scss/Components/TextCta/text-cta.scss +1 -1
  164. package/scss/Components/TextCta/textcta.scss +1 -1
  165. package/scss/Components/UserFeedback/user-feedback.scss +1 -1
  166. package/scss/Molecules/ImageCaption/image-caption.scss +17 -3
  167. package/scss/Molecules/SectionHeader/section-header.scss +1 -1
  168. package/scss/Molecules/SectionHeader/sectionheader.scss +1 -1
  169. package/scss/Patterns/HubHeader/hub-header.scss +1 -1
  170. package/scss/Patterns/RichContent/rich-content.scss +177 -0
  171. package/scss/Patterns/_shared/pattern-demo.scss +6 -1
  172. package/scss/Utilities/EmbedContainer/embed-container.scss +1 -1
  173. package/scss/Utilities/FullWidth/full-width.scss +1 -1
  174. package/scss/Utilities/Loader/loader.scss +133 -12
  175. package/scss/Utilities/Normalize/normalize.scss +1 -1
  176. package/scss/Utilities/PagePatterns/page-patterns.scss +1 -1
  177. package/scss/Utilities/ShowMore/show-more.scss +7 -1
  178. package/scss/Utilities/SkipLink/skip-link.scss +1 -1
  179. package/scss/assets/scss/_components.scss +5 -0
  180. package/scss/assets/scss/_foundational.scss +23 -8
  181. package/scss/assets/scss/_mixins.scss +5 -0
  182. package/scss/assets/scss/_variables.scss +2 -2
@@ -107,6 +107,7 @@ export function mgOnThisPageNav(scope) {
107
107
  container._mgOnThisPageNavAbort = ac;
108
108
 
109
109
  setupClickHandlers(container, ac.signal);
110
+ setupBlockSize(container, ac.signal);
110
111
  setupScrollSpy(container);
111
112
  setupFocusScrolling(container, ac.signal);
112
113
  setupScrollButtons(container, ac.signal);
@@ -132,13 +133,53 @@ export function mgOnThisPageNavDestroy(container) {
132
133
  container._mgOnThisPageNavResizeObserver.disconnect();
133
134
  delete container._mgOnThisPageNavResizeObserver;
134
135
  }
136
+ if (container._mgOnThisPageNavSizeObserver) {
137
+ container._mgOnThisPageNavSizeObserver.disconnect();
138
+ delete container._mgOnThisPageNavSizeObserver;
139
+ document.documentElement.style.removeProperty(BLOCK_SIZE);
140
+ }
135
141
  container
136
142
  .querySelectorAll('.mg-on-this-page-nav__scroll-btn')
137
143
  .forEach(btn => btn.remove());
138
- container.classList.remove('mg-on-this-page-nav--has-left-overflow');
144
+ container.classList.remove(
145
+ 'mg-on-this-page-nav--has-left-overflow',
146
+ 'mg-on-this-page-nav--has-right-overflow'
147
+ );
139
148
  delete container.dataset.mgOnThisPageNavInitialized;
140
149
  }
141
150
 
151
+ const BLOCK_SIZE = '--mg-on-this-page-nav-block-size';
152
+
153
+ /**
154
+ * Publishes the bar's height on the root element while it is sticky, and 0px
155
+ * when it is not (it un-sticks on short viewports). The stylesheet adds it to
156
+ * `scroll-padding-top`, so anything the browser scrolls into view (focus,
157
+ * anchors, scrollIntoView from other components) lands below the bar rather
158
+ * than under it.
159
+ *
160
+ * @param {HTMLElement} container - The nav element
161
+ * @param {AbortSignal} signal - Removes the resize listener on destroy
162
+ */
163
+ function setupBlockSize(container, signal) {
164
+ const root = document.documentElement;
165
+ const publish = () => {
166
+ const sticky = getComputedStyle(container).position === 'sticky';
167
+ root.style.setProperty(
168
+ BLOCK_SIZE,
169
+ `${sticky ? container.offsetHeight || 0 : 0}px`
170
+ );
171
+ };
172
+ publish();
173
+ window.addEventListener('resize', publish, { passive: true, signal });
174
+ if (typeof ResizeObserver === 'function') {
175
+ const observer = new ResizeObserver(publish);
176
+ observer.observe(container);
177
+ container._mgOnThisPageNavSizeObserver = observer;
178
+ } else {
179
+ container._mgOnThisPageNavSizeObserver = { disconnect() {} };
180
+ }
181
+ }
182
+
142
183
  /**
143
184
  * Auto-detect mode: scan headings from the page and generate nav links.
144
185
  * Preserves any existing CTA element in the container.
@@ -399,6 +440,13 @@ function updateScrollButtons(container, list, prevBtn, nextBtn) {
399
440
  'mg-on-this-page-nav--has-left-overflow',
400
441
  rtl ? canScrollFurther : scrolledFromStart
401
442
  );
443
+ // has-right-overflow is its mirror. Only the RTL fades read it: in RTL the
444
+ // hidden content can sit on either physical side, and a fade belongs only
445
+ // on a side with something hidden behind it.
446
+ container.classList.toggle(
447
+ 'mg-on-this-page-nav--has-right-overflow',
448
+ rtl ? scrolledFromStart : canScrollFurther
449
+ );
402
450
  }
403
451
 
404
452
  /**
@@ -543,6 +591,15 @@ function setupScrollSpy(container) {
543
591
  if (targets.length === 0) return;
544
592
 
545
593
  let activeId = null;
594
+
595
+ // At the foot of a page that scrolls a real distance. On a page that barely
596
+ // scrolls, the bottom is also the top, and the first section is right.
597
+ const atFoot = () => {
598
+ const doc = document.documentElement;
599
+ const scrollable = doc.scrollHeight - window.innerHeight;
600
+ const minimum = Math.max(120, window.innerHeight * 0.25);
601
+ return scrollable >= minimum && window.scrollY >= scrollable - 2;
602
+ };
546
603
  // Track which targets are currently intersecting (avoids DOM expando properties)
547
604
  const intersecting = new Set();
548
605
 
@@ -584,6 +641,13 @@ function setupScrollSpy(container) {
584
641
  }
585
642
  });
586
643
 
644
+ // At the foot of the page the last section wins (see onScroll). The
645
+ // observer reports after the scroll event, so without this check it
646
+ // would take the mark straight back.
647
+ if (atFoot()) {
648
+ setActive(targets[targets.length - 1].id);
649
+ return;
650
+ }
587
651
  // Pick the first intersecting target in DOM order
588
652
  const firstVisible = targets.find(t => intersecting.has(t));
589
653
  if (firstVisible) {
@@ -599,12 +663,27 @@ function setupScrollSpy(container) {
599
663
  targets.forEach(target => observer.observe(target));
600
664
  container._mgOnThisPageNavObserver = observer;
601
665
 
666
+ // A short last section can never scroll up into the observed band, so at
667
+ // the foot of the page the previous section stayed marked, even straight
668
+ // after following the last link. At the bottom, mark the last target. Only
669
+ // on pages that scroll a real distance: on one that barely scrolls, the
670
+ // bottom is also the top, and the first section is the right answer.
671
+ const onScroll = () => {
672
+ if (atFoot()) setActive(targets[targets.length - 1].id);
673
+ };
674
+ window.addEventListener('scroll', onScroll, {
675
+ passive: true,
676
+ signal: container._mgOnThisPageNavAbort?.signal,
677
+ });
678
+
602
679
  const hashId = window.location.hash ? window.location.hash.slice(1) : null;
603
680
  if (hashId && linkMap.has(hashId)) {
604
681
  setActive(hashId);
605
682
  } else if (targets.length > 0) {
606
683
  setActive(targets[0].id);
607
684
  }
685
+ // A page restored at its foot (reload, back button) is checked straight away.
686
+ onScroll();
608
687
  }
609
688
 
610
689
  // Auto-wrap so the browser Event object is not passed as scope
package/js/show-more.js CHANGED
@@ -61,6 +61,58 @@ function resolveTarget(toggle, selector) {
61
61
  return document.querySelector(selector);
62
62
  }
63
63
 
64
+ /**
65
+ * The height the content collapses to, read from CSS: the computed
66
+ * max-height of the collapsed state, so `--mg-show-more-height` may be set in
67
+ * any unit, including `lh`. Applying and removing the class in one task
68
+ * forces a style recalculation but never paints.
69
+ *
70
+ * @param {HTMLElement} target - The collapsible content.
71
+ * @returns {number} Pixels, or NaN when CSS gives no length.
72
+ */
73
+ function collapsedHeight(target) {
74
+ const collapsed = target.classList.contains('mg-show-more--collapsed');
75
+ if (!collapsed) target.classList.add('mg-show-more--collapsed');
76
+ const height = parseFloat(getComputedStyle(target).maxHeight);
77
+ if (!collapsed) target.classList.remove('mg-show-more--collapsed');
78
+ return height;
79
+ }
80
+
81
+ /**
82
+ * @param {HTMLElement} target - The collapsible content.
83
+ * @returns {number} Its line height in pixels.
84
+ */
85
+ function lineHeight(target) {
86
+ const style = getComputedStyle(target);
87
+ const line = parseFloat(style.lineHeight);
88
+ return Number.isFinite(line) ? line : parseFloat(style.fontSize) * 1.2;
89
+ }
90
+
91
+ /**
92
+ * After a reader collapses content, bring the start of it back into view if
93
+ * the toggle has left the viewport. Scrolls the heading just before the
94
+ * content when there is one, honours scroll-padding (so a sticky bar such as
95
+ * On this page is allowed for) and reduced motion.
96
+ *
97
+ * @param {HTMLElement} toggle - The toggle that was used.
98
+ * @param {HTMLElement} target - The content it collapsed.
99
+ */
100
+ function returnToContent(toggle, target) {
101
+ const box = toggle.getBoundingClientRect();
102
+ if (box.bottom > 0 && box.top < window.innerHeight) return;
103
+ const previous = target.previousElementSibling;
104
+ const anchor = previous?.matches('h1, h2, h3, h4, h5, h6')
105
+ ? previous
106
+ : target;
107
+ const reduce =
108
+ typeof window.matchMedia === 'function' &&
109
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
110
+ anchor.scrollIntoView?.({
111
+ block: 'start',
112
+ behavior: reduce ? 'auto' : 'smooth',
113
+ });
114
+ }
115
+
64
116
  /**
65
117
  * Initializes "show more" toggle buttons.
66
118
  *
@@ -102,7 +154,6 @@ export function mgShowMore(scope) {
102
154
  'mg-show-more--collapsed'
103
155
  );
104
156
  const wasOpen = item.classList.contains('mg-show-more--button--open');
105
- const initialText = item.textContent;
106
157
  const initialControls = item.getAttribute('aria-controls');
107
158
  const initialRole = item.getAttribute('role');
108
159
  const onKeydown = event => {
@@ -112,8 +163,27 @@ export function mgShowMore(scope) {
112
163
  }
113
164
  };
114
165
 
115
- item.dataset.dataVfGoogleAnalyticsLabel =
116
- 'Show more: ' + (item.dataset.mgShowMoreLabelCollapsed || 'Show more');
166
+ // The click tracker reads data-vf-analytics-label. Report the action the
167
+ // toggle is about to perform, in fixed English so reports do not split by
168
+ // language. A label the author set by hand is kept as it is, unless
169
+ // per-state labels are given.
170
+ const COLLAPSED = 'mg-show-more--collapsed';
171
+ const authorLabel = item.getAttribute('data-vf-analytics-label');
172
+ const { mgShowMoreAnalyticsCollapsed, mgShowMoreAnalyticsOpen } =
173
+ item.dataset;
174
+ const stateLabels =
175
+ mgShowMoreAnalyticsCollapsed || mgShowMoreAnalyticsOpen || !authorLabel
176
+ ? {
177
+ collapsed: mgShowMoreAnalyticsCollapsed || 'Show more',
178
+ open: mgShowMoreAnalyticsOpen || 'Show less',
179
+ }
180
+ : null;
181
+
182
+ // Visible text lives in a [data-mg-show-more-label] child when there is
183
+ // one, so visually hidden context beside it survives each toggle.
184
+ const labelNode = item.querySelector('[data-mg-show-more-label]') || item;
185
+ const initialText = labelNode.textContent;
186
+ const initialHidden = item.hidden;
117
187
 
118
188
  // Relate the toggle to the content it acts on. A <button> already
119
189
  // announces as a button; an anchor copied from an earlier version of the
@@ -131,24 +201,35 @@ export function mgShowMore(scope) {
131
201
  item.addEventListener('keydown', onKeydown);
132
202
  }
133
203
 
134
- const onClick = event => {
135
- // Also neutralises the dead `href="#"` on a legacy anchor toggle.
136
- event.preventDefault();
137
- mgShowMoreTarget.classList.toggle('mg-show-more--collapsed');
138
-
139
- // Which label to show?
140
- item.textContent = mgShowMoreTarget.classList.contains(
141
- 'mg-show-more--collapsed'
142
- )
204
+ // Initialisation and re-measures change state here rather than through
205
+ // item.click(), so they reach no click listener (analytics reporting, the
206
+ // Drupal wrapper). Focus entering, or a click on, collapsed content still
207
+ // clicks the toggle below: those stand for a reader's action.
208
+ const setCollapsed = collapsed => {
209
+ mgShowMoreTarget.classList.toggle(COLLAPSED, collapsed);
210
+ labelNode.textContent = collapsed
143
211
  ? item.dataset.mgShowMoreLabelCollapsed || 'Show more'
144
212
  : item.dataset.mgShowMoreLabelOpen || 'Show less';
145
-
146
- if (mgShowMoreTarget.classList.contains('mg-show-more--collapsed')) {
147
- item.classList.remove('mg-show-more--button--open');
148
- } else {
149
- item.classList.add('mg-show-more--button--open');
213
+ item.classList.toggle('mg-show-more--button--open', !collapsed);
214
+ if (stateLabels) {
215
+ item.setAttribute(
216
+ 'data-vf-analytics-label',
217
+ collapsed ? stateLabels.collapsed : stateLabels.open
218
+ );
150
219
  }
151
220
  };
221
+
222
+ let readerToggled = false;
223
+ const onClick = event => {
224
+ // Also neutralises the dead `href="#"` on a legacy anchor toggle.
225
+ event.preventDefault();
226
+ readerToggled = true;
227
+ const collapsing = !mgShowMoreTarget.classList.contains(COLLAPSED);
228
+ setCollapsed(collapsing);
229
+ // Collapsing a long block pulls the toggle up the page, often out of
230
+ // view. Bring the reader back to the start of what they collapsed.
231
+ if (collapsing) returnToContent(item, mgShowMoreTarget);
232
+ };
152
233
  item.addEventListener('click', onClick);
153
234
 
154
235
  // The collapse is `max-height` + `overflow: hidden`, which clips content
@@ -158,7 +239,7 @@ export function mgShowMore(scope) {
158
239
  // entry keeps the focused control visible.
159
240
  const onFocusin = event => {
160
241
  if (item === event.target || item.contains(event.target)) return;
161
- if (mgShowMoreTarget.classList.contains('mg-show-more--collapsed')) {
242
+ if (mgShowMoreTarget.classList.contains(COLLAPSED)) {
162
243
  item.click();
163
244
  }
164
245
  };
@@ -167,14 +248,61 @@ export function mgShowMore(scope) {
167
248
  // Allow items to be shown by clicking anywhere on the collapsed item
168
249
  // https://gitlab.com/undrr/web-backlog/-/issues/1612
169
250
  const onTargetClick = () => {
170
- if (mgShowMoreTarget.classList.contains('mg-show-more--collapsed')) {
251
+ if (mgShowMoreTarget.classList.contains(COLLAPSED)) {
171
252
  item.click();
172
253
  }
173
254
  };
174
255
  mgShowMoreTarget.addEventListener('click', onTargetClick);
175
256
 
176
- item.click();
257
+ // A template may ship the toggle `hidden` so it never shows without
258
+ // JavaScript; the content is then simply open.
259
+ item.hidden = false;
260
+ setCollapsed(!wasCollapsed);
261
+
262
+ // Opt-in: collapse only when enough would be hidden. Short content stays
263
+ // open and the toggle is hidden, so a template can wrap every body field
264
+ // in ShowMore. The value is a number of lines (empty means 1): collapse
265
+ // when the content runs at least that many lines past the collapsed
266
+ // height. The collapsed height is read from CSS (--mg-show-more-height),
267
+ // so the decision does not depend on the current state and cannot flip
268
+ // back and forth. Measured again when the content resizes, until the
269
+ // reader uses the toggle. Content with no layout yet (inside a closed
270
+ // tab, say) waits until it has one.
271
+ let observer = null;
272
+ let frame = 0;
273
+ if (item.hasAttribute('data-mg-show-more-min-hidden-lines')) {
274
+ const lines =
275
+ parseFloat(item.dataset.mgShowMoreMinHiddenLines) > 0
276
+ ? parseFloat(item.dataset.mgShowMoreMinHiddenLines)
277
+ : 1;
278
+ const measure = () => {
279
+ frame = 0;
280
+ if (readerToggled || mgShowMoreTarget.scrollHeight === 0) return;
281
+ const limit = collapsedHeight(mgShowMoreTarget);
282
+ const line = lineHeight(mgShowMoreTarget);
283
+ if (!Number.isFinite(limit) || !Number.isFinite(line)) return;
284
+ const collapse = mgShowMoreTarget.scrollHeight - limit >= lines * line;
285
+ setCollapsed(collapse);
286
+ item.hidden = !collapse;
287
+ };
288
+ measure();
289
+ if (typeof ResizeObserver === 'function') {
290
+ // Deferred to the next frame: changing the observed box inside the
291
+ // callback is what raises "ResizeObserver loop" errors.
292
+ observer = new ResizeObserver(() => {
293
+ if (frame) cancelAnimationFrame(frame);
294
+ frame = requestAnimationFrame(measure);
295
+ });
296
+ observer.observe(mgShowMoreTarget);
297
+ }
298
+ }
299
+
177
300
  cleanups.push(() => {
301
+ observer?.disconnect();
302
+ if (frame) cancelAnimationFrame(frame);
303
+ item.hidden = initialHidden;
304
+ if (authorLabel === null) item.removeAttribute('data-vf-analytics-label');
305
+ else item.setAttribute('data-vf-analytics-label', authorLabel);
178
306
  item.removeEventListener('click', onClick);
179
307
  item.removeEventListener('keydown', onKeydown);
180
308
  mgShowMoreTarget.removeEventListener('focusin', onFocusin);
@@ -184,7 +312,7 @@ export function mgShowMore(scope) {
184
312
  wasCollapsed
185
313
  );
186
314
  item.classList.toggle('mg-show-more--button--open', wasOpen);
187
- item.textContent = initialText;
315
+ labelNode.textContent = initialText;
188
316
  for (const [attribute, value] of [
189
317
  ['aria-controls', initialControls],
190
318
  ['role', initialRole],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@undrr/undrr-mangrove",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Mangrove design System for UNDRR",
5
5
  "files": [
6
6
  "components/**/*",
@@ -9,6 +9,7 @@
9
9
  "scss/**/*.scss",
10
10
  "error-pages/**/*",
11
11
  "fonts/**/*",
12
+ "docs/**/*",
12
13
  "!**/__tests__/**",
13
14
  "!**/__snapshots__/**",
14
15
  "!**/__mocks__/**",
@@ -20,6 +21,10 @@
20
21
  "type": "git",
21
22
  "url": "https://github.com/unisdr/undrr-mangrove.git"
22
23
  },
24
+ "homepage": "https://mangrove.undrr.org/",
25
+ "bugs": {
26
+ "url": "https://github.com/PreventionWeb/undrr-mangrove/issues"
27
+ },
23
28
  "keywords": [],
24
29
  "author": "",
25
30
  "license": "Apache-2.0"
@@ -4,7 +4,7 @@
4
4
  * Styles for HTML <abbr> elements with subtle dotted underlines and accessible expansion styling.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
8
8
  */
9
9
  abbr[title] {
10
10
  border-bottom: 2px dashed rgb(var(--mg-color-red-900));
@@ -4,7 +4,7 @@
4
4
  * Typography and border accent styles for standard HTML <blockquote> elements.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
8
8
  */
9
9
  .mg-body blockquote,
10
10
  .mg-content blockquote {
@@ -4,7 +4,7 @@
4
4
  * Typographic styles for HTML <cite> citation references.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
8
8
  */
9
9
  .mg-body cite,
10
10
  .mg-content cite {
@@ -4,7 +4,7 @@
4
4
  * Monospace typographic styling for inline <code> elements.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
8
8
  */
9
9
  .mg-code,
10
10
  code {
@@ -4,7 +4,7 @@
4
4
  * Highlight styling for HTML <mark> elements using theme-aware highlight colors.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
8
8
  */
9
9
  mark {
10
10
  background: var(--mg-color-yellow-light);
@@ -4,7 +4,7 @@
4
4
  * Inline quotation typography and glyph styles for <q> elements.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
8
8
  */
9
9
  q {
10
10
  font-style: italic;
@@ -4,7 +4,7 @@
4
4
  * Reduced scale typography styles for fine print and secondary metadata (<small>).
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/BaseTypography/BaseTypography.mdx
8
8
  */
9
9
  small {
10
10
  @extend %paragraph-font-100;
@@ -4,6 +4,6 @@
4
4
  * Color swatch and background/text utility helper classes across brand palettes.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/Colors/color.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/Colors/color.mdx
8
8
  */
9
9
  // empty
@@ -4,7 +4,7 @@
4
4
  * Placeholder presentation for empty search results, missing data, or filtered-out views.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/EmptyState/EmptyState.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/EmptyState/EmptyState.mdx
8
8
  */
9
9
  // Empty state — what a list, table, chart or dashboard tile shows when it has
10
10
  // no content to show.
@@ -5,10 +5,10 @@
5
5
  * Generated by scripts/icon-build.cjs — do not edit by hand.
6
6
  *
7
7
  * Full documentation:
8
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/Icons/Icons.mdx
8
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/Icons/Icons.mdx
9
9
  */
10
10
  // To regenerate: yarn build:icons
11
- // 98 icons from Lucide, OCHA, and custom SVG sources.
11
+ // 106 icons from Lucide, OCHA, and custom SVG sources.
12
12
 
13
13
  .mg-icon-globe::before {
14
14
  --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10'/><path d='M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20M2 12h20'/></svg>");
@@ -55,7 +55,7 @@
55
55
  }
56
56
 
57
57
  .mg-icon-pen-nib::before {
58
- --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><path d='M15.707 21.293a1 1 0 0 1-1.414 0l-1.586-1.586a1 1 0 0 1 0-1.414l5.586-5.586a1 1 0 0 1 1.414 0l1.586 1.586a1 1 0 0 1 0 1.414z'/><path d='m18 13-1.375-6.874a1 1 0 0 0-.746-.776L3.235 2.028a1 1 0 0 0-1.207 1.207L5.35 15.879a1 1 0 0 0 .776.746L13 18M2.3 2.3l7.286 7.286'/><circle cx='11' cy='11' r='2'/></svg>");
58
+ --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><path d='m18 13-.654-4.736a4 4 0 0 0-2.756-3.266l-9.163-2.9a2 2 0 0 0-2.017.493l-.82.819a2 2 0 0 0-.492 2.017l2.9 9.163a4 4 0 0 0 3.266 2.756L13 18'/><path d='M18.648 12.352a1.205 1.205 0 0 1 1.704 0l1.296 1.296a1.205 1.205 0 0 1 0 1.704l-6.296 6.296a1.205 1.205 0 0 1-1.704 0l-1.296-1.296a1.205 1.205 0 0 1 0-1.704zM3 3l6.586 6.586'/><circle cx='11' cy='11' r='2'/></svg>");
59
59
  }
60
60
 
61
61
  .mg-icon-code-branch::before {
@@ -126,6 +126,38 @@
126
126
  --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><path d='M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8'/><path d='M21 3v5h-5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16'/><path d='M8 16H3v5'/></svg>");
127
127
  }
128
128
 
129
+ .mg-icon-plus::before {
130
+ --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><path d='M5 12h14M12 5v14'/></svg>");
131
+ }
132
+
133
+ .mg-icon-minus::before {
134
+ --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><path d='M5 12h14'/></svg>");
135
+ }
136
+
137
+ .mg-icon-pause::before {
138
+ --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><rect width='5' height='18' x='14' y='3' rx='1'/><rect width='5' height='18' x='5' y='3' rx='1'/></svg>");
139
+ }
140
+
141
+ .mg-icon-play::before {
142
+ --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><path d='M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z'/></svg>");
143
+ }
144
+
145
+ .mg-icon-circle-minus::before {
146
+ --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10'/><path d='M8 12h8'/></svg>");
147
+ }
148
+
149
+ .mg-icon-expand::before {
150
+ --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><path d='m15 15 6 6M15 9l6-6M21 16v5h-5M21 8V3h-5M3 16v5h5M3 21l6-6M3 8V3h5M9 9 3 3'/></svg>");
151
+ }
152
+
153
+ .mg-icon-shrink::before {
154
+ --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><path d='m15 15 6 6m-6-6v4.8m0-4.8h4.8M9 19.8V15m0 0H4.2M9 15l-6 6M15 4.2V9m0 0h4.8M15 9l6-6M9 4.2V9m0 0H4.2M9 9 3 3'/></svg>");
155
+ }
156
+
157
+ .mg-icon-layers::before {
158
+ --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><path d='M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z'/><path d='M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12'/><path d='M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17'/></svg>");
159
+ }
160
+
129
161
  .mg-icon-clone::before {
130
162
  --mg-icon-svg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'><rect width='14' height='14' x='8' y='8' rx='2' ry='2'/><path d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2'/></svg>");
131
163
  }
@@ -4,7 +4,7 @@
4
4
  * Base styles, icon sizing, font-family setup, and alignment classes for Mangrove icons.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/Icons/Icons.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/Icons/Icons.mdx
8
8
  */
9
9
  // Mangrove Icon System
10
10
  // Icons render via CSS mask-image backed by SVG data URIs (Lucide, OCHA, custom sources).
@@ -4,7 +4,7 @@
4
4
  * Circular avatar and author profile photo styling with standardized sizing and object-fit.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/Images/AuthorImage/AuthorImages.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/Images/AuthorImage/AuthorImages.mdx
8
8
  */
9
9
  /* author image */
10
10
 
@@ -49,8 +49,8 @@
49
49
  position: absolute;
50
50
  z-index: 1;
51
51
  transition:
52
- opacity 0.2s ease,
53
- transform 0.2s ease;
52
+ opacity var(--mg-motion-duration-fast) var(--mg-motion-easing-standard),
53
+ transform var(--mg-motion-duration-fast) var(--mg-motion-easing-standard);
54
54
  transform: translateY(2px);
55
55
  }
56
56
 
@@ -224,3 +224,10 @@
224
224
  justify-items: center;
225
225
  text-align: center;
226
226
  }
227
+
228
+ @media (prefers-reduced-motion: reduce) {
229
+ .mg-author-image__avatar::before {
230
+ transition: none;
231
+ transform: none;
232
+ }
233
+ }
@@ -4,7 +4,7 @@
4
4
  * Fluid responsive image wrapper and image sizing utilities.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/Images/ImageCaptionCredit/ImageCaptionCredit.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/Images/ImageCaptionCredit/ImageCaptionCredit.mdx
8
8
  */
9
9
  /* img start */
10
10
  img,
@@ -4,7 +4,7 @@
4
4
  * Figure, figcaption, caption text, and photo credit metadata presentation styles.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/Images/ImageCaptionCredit/ImageCaptionCredit.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/Images/ImageCaptionCredit/ImageCaptionCredit.mdx
8
8
  */
9
9
  // @import '../../../assets/scss/base';
10
10
 
@@ -49,7 +49,8 @@
49
49
  padding-bottom: 0;
50
50
  width: 25%;
51
51
 
52
- p {
52
+ p,
53
+ :where(.mg-image-caption__text) {
53
54
  font-size: var(--mg-font-size-200);
54
55
  line-height: 1.4;
55
56
  margin-bottom: var(--mg-spacing-50);
@@ -61,6 +62,10 @@
61
62
  padding: var(--mg-spacing-50) 0 0 0;
62
63
  }
63
64
  }
65
+
66
+ .mg-image-caption__text p:last-child {
67
+ margin-bottom: 0;
68
+ }
64
69
  }
65
70
  }
66
71
 
@@ -4,7 +4,7 @@
4
4
  * Subtle attribution and copyright credit line styling for imagery.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/Images/ImageCaptionCredit/ImageCaptionCredit.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/Images/ImageCaptionCredit/ImageCaptionCredit.mdx
8
8
  */
9
9
  .mg-credits {
10
10
  font-size: var(--mg-font-size-200);
@@ -4,7 +4,7 @@
4
4
  * Responsive layout containers with breakpoint-driven max widths (480/900/1164/1440px) and gutter padding.
5
5
  *
6
6
  * Full documentation:
7
- * https://github.com/unisdr/undrr-mangrove/blob/main/stories/Atom/Layout/Container/Container.mdx
7
+ * https://github.com/PreventionWeb/undrr-mangrove/blob/main/stories/Atom/Layout/Container/Container.mdx
8
8
  */
9
9
  // Define your breakpoints if not already defined
10
10
  // $mg-breakpoint-mobile: 480px;