@icij/murmur-next 4.8.2 → 4.8.4

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.
@@ -7,7 +7,8 @@ export declare const modalDecorator: (buttonLabel: string | undefined, modalTitl
7
7
  BModal: any;
8
8
  };
9
9
  setup(): {
10
- show: (id?: import('bootstrap-vue-next').ControllerKey) => void;
10
+ visible: any;
11
+ show: () => void;
11
12
  buttonLabel: string;
12
13
  modalTitle: string | null;
13
14
  size: keyof import('bootstrap-vue-next').BaseSize;
@@ -93,10 +93,11 @@
93
93
  <b-popover
94
94
  v-model="showFollowUsPopover"
95
95
  target="follow-us-toggler"
96
- placement="bottom-start"
96
+ placement="bottom-end"
97
97
  click
98
98
  >
99
99
  <follow-us-popover
100
+ :compact="compactSignUp"
100
101
  @update:close="closeFollowUsPopover"
101
102
  @keydown.esc="closeFollowUsPopover"
102
103
  />
@@ -135,13 +136,18 @@ export interface AppHeaderProps {
135
136
  * Target link of the donate button.
136
137
  */
137
138
  donateUrl?: string
139
+ /**
140
+ * Compact layout for the sign-up form in the follow-us popover.
141
+ */
142
+ compactSignUp?: boolean
138
143
  }
139
144
 
140
145
  const props = withDefaults(defineProps<AppHeaderProps>(), {
141
146
  position: 'fixed',
142
147
  noHeadroom: false,
143
148
  homeUrl: () => config.get('app.home'),
144
- donateUrl: () => config.get('app.donate-url')
149
+ donateUrl: () => config.get('app.donate-url'),
150
+ compactSignUp: false
145
151
  })
146
152
 
147
153
  const { t } = useI18n()
@@ -4,9 +4,10 @@
4
4
  :href="homeUrl"
5
5
  target="_blank"
6
6
  class="text-white embeddable-footer__brand"
7
+ :class="{ 'embeddable-footer__brand--no-divider': hideDivider }"
7
8
  >
8
9
  <brand
9
- :size="40"
10
+ :size="logoHeight"
10
11
  no-border
11
12
  class="me-2"
12
13
  color="white"
@@ -82,6 +83,14 @@ export interface EmbeddableFooterProps {
82
83
  * Sharing option values to bind to the sharing-options component in the bottom-right corner.
83
84
  */
84
85
  sharingOptionsValues?: Record<string, unknown>
86
+ /**
87
+ * Hide the divider (right border) next to the brand.
88
+ */
89
+ hideDivider?: boolean
90
+ /**
91
+ * Height of the logo in pixels.
92
+ */
93
+ logoHeight?: number | string
85
94
  }
86
95
 
87
96
  withDefaults(defineProps<EmbeddableFooterProps>(), {
@@ -90,7 +99,9 @@ withDefaults(defineProps<EmbeddableFooterProps>(), {
90
99
  iframeMinHeight: 100,
91
100
  iframeMinWidth: 100,
92
101
  homeUrl: () => config.get('app.home'),
93
- sharingOptionsValues: () => ({})
102
+ sharingOptionsValues: () => ({}),
103
+ hideDivider: false,
104
+ logoHeight: 40
94
105
  })
95
106
 
96
107
  // Reactive state
@@ -103,7 +114,6 @@ onMounted(() => {
103
114
  </script>
104
115
 
105
116
  <style lang="scss" scoped>
106
-
107
117
  @import '../../styles/mixins';
108
118
 
109
119
  @include keyframes(slideup) {
@@ -146,6 +156,10 @@ onMounted(() => {
146
156
  display: flex;
147
157
  justify-content: center;
148
158
  align-items: center;
159
+
160
+ &--no-divider {
161
+ border-right: none;
162
+ }
149
163
  }
150
164
  &__lead {
151
165
  flex-grow: 1;
@@ -176,7 +190,7 @@ onMounted(() => {
176
190
  right: 0;
177
191
  margin: $spacer * 0.25;
178
192
 
179
- .sharing-options__link {
193
+ &:deep(.sharing-options__link) {
180
194
  opacity: 0;
181
195
  animation: slideup 200ms forwards;
182
196
  @include animation-delay-loop(0, 10, 50ms);
@@ -4,9 +4,14 @@
4
4
  class="btn btn-link text-secondary follow-us__close"
5
5
  @click="closeSignupPopover"
6
6
  >
7
- <app-icon><i-ph-x /></app-icon>
7
+ <app-icon>
8
+ <i-ph-x-bold />
9
+ </app-icon>
8
10
  </button>
9
- <sign-up-form class="p-3" />
11
+ <sign-up-form
12
+ class="p-3"
13
+ :compact="compact"
14
+ />
10
15
  <div class="px-3 pb-1 text-uppercase text-muted fw-bold">
11
16
  {{ t('follow-us-popover.heading') }}
12
17
  </div>
@@ -59,9 +64,17 @@ import { useI18n } from 'vue-i18n'
59
64
  import SignUpForm from '@/components/Form/FormSignUp.vue'
60
65
  import AppIcon from '@/components/App/AppIcon.vue'
61
66
 
62
- /**
63
- * FollowUsPopover
64
- */
67
+ export interface FollowUsPopoverProps {
68
+ /**
69
+ * Compact layout for the sign-up form.
70
+ */
71
+ compact?: boolean
72
+ }
73
+
74
+ withDefaults(defineProps<FollowUsPopoverProps>(), {
75
+ compact: false
76
+ })
77
+
65
78
  const emit = defineEmits(['update:close'])
66
79
  const { t } = useI18n()
67
80
 
@@ -64,7 +64,7 @@ export interface FormAdvancedLinkProps {
64
64
 
65
65
  const props = withDefaults(defineProps<FormAdvancedLinkProps>(), {
66
66
  link: undefined,
67
- title: null,
67
+ title: 'Link',
68
68
  forms: () => ['raw', 'markdown', 'rich', 'html'],
69
69
  card: false,
70
70
  pills: false,
@@ -137,7 +137,7 @@ function onUpdate(event: string | undefined): void {
137
137
  :title="tab.title"
138
138
  >
139
139
  <advanced-link-form-tab
140
- :title="tab.title"
140
+ :title="title"
141
141
  :type="tab.type"
142
142
  :data-type="tab.type"
143
143
  :compact="small"
@@ -2,6 +2,7 @@
2
2
  import { computed, ref } from 'vue'
3
3
 
4
4
  import { useI18n } from 'vue-i18n'
5
+ import { BFormCheckbox } from 'bootstrap-vue-next'
5
6
  import HapticCopy from '@/components/HapticCopy/HapticCopy.vue'
6
7
  import IframeResizer from '@/utils/iframe-resizer'
7
8
 
@@ -38,6 +39,10 @@ export interface FormEmbedProps {
38
39
  * URL of the iframe code
39
40
  */
40
41
  url?: string | null
42
+ /**
43
+ * Use a switch display for the responsive iframe checkbox
44
+ */
45
+ switchResponsive?: boolean
41
46
  }
42
47
 
43
48
  const props = withDefaults(defineProps<FormEmbedProps>(), {
@@ -47,7 +52,8 @@ const props = withDefaults(defineProps<FormEmbedProps>(), {
47
52
  height: () => window.innerHeight,
48
53
  minWidth: 0,
49
54
  minHeight: 0,
50
- url: null
55
+ url: null,
56
+ switchResponsive: false
51
57
  })
52
58
 
53
59
  const { t } = useI18n()
@@ -107,20 +113,14 @@ function embedCode(withPym = responsiveCheck.value): string {
107
113
  />
108
114
 
109
115
  <div class="d-flex justify-content-between">
110
- <div class="form-check align-self-end">
111
- <input
112
- id="responsiveOptin"
113
- v-model="responsiveCheck"
114
- type="checkbox"
115
- class="form-check-input"
116
- >
117
- <label
118
- class="form-check-label fw-bold"
119
- for="responsiveOptin"
120
- >
121
- {{ t('embed-form.responsive-optin') }}
122
- </label>
123
- </div>
116
+ <b-form-checkbox
117
+ id="responsiveOptin"
118
+ v-model="responsiveCheck"
119
+ class="align-self-end fw-bold"
120
+ :switch="switchResponsive"
121
+ >
122
+ {{ t('embed-form.responsive-optin') }}
123
+ </b-form-checkbox>
124
124
 
125
125
  <haptic-copy
126
126
  class="btn-link btn-sm text-uppercase fw-bold"
@@ -1,13 +1,13 @@
1
1
  <template>
2
2
  <form
3
3
  class="sign-up-form"
4
- :class="{ 'sign-up-form--horizontal': horizontal }"
4
+ :class="classList"
5
5
  @submit.prevent="subscribe"
6
6
  >
7
7
  <fieldset :disabled="frozen">
8
8
  <label
9
9
  v-if="!noLabel"
10
- class="text-uppercase text-muted fw-bold"
10
+ class="sign-up-form__fieldset__label text-uppercase text-muted fw-bold"
11
11
  for="input-email"
12
12
  >
13
13
  {{ t('sign-up-form.label') }}
@@ -90,6 +90,10 @@ export interface FormSignUpProps {
90
90
  * Color variant of the sign-up button
91
91
  */
92
92
  variant?: ButtonVariant
93
+ /**
94
+ * Compact layout with no gap between the input and the submit button.
95
+ */
96
+ compact?: boolean
93
97
  }
94
98
 
95
99
  const props = withDefaults(defineProps<FormSignUpProps>(), {
@@ -100,7 +104,8 @@ const props = withDefaults(defineProps<FormSignUpProps>(), {
100
104
  horizontal: false,
101
105
  tracker: () => config.get('signup-form.tracker'),
102
106
  referrer: null,
103
- variant: 'primary'
107
+ variant: 'primary',
108
+ compact: false
104
109
  })
105
110
  const emit = defineEmits(['error', 'success', 'subscribed'])
106
111
  const { t } = useI18n()
@@ -117,6 +122,11 @@ const { send } = useSendEmail(
117
122
  props.defaultGroups
118
123
  )
119
124
 
125
+ const classList = computed(() => ({
126
+ 'sign-up-form--horizontal': props.horizontal,
127
+ 'sign-up-form--compact': props.compact,
128
+ }))
129
+
120
130
  const variantColorClass = computed(() => {
121
131
  return `btn-${props.variant}`
122
132
  })
@@ -170,6 +180,32 @@ function unfreeze() {
170
180
  <style lang="scss">
171
181
 
172
182
  .sign-up-form {
183
+ .sign-up-form__fieldset {
184
+ &__label {
185
+ margin-bottom: $spacer-xs;
186
+ }
187
+
188
+ &__group {
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: $spacer-xs;
192
+ }
193
+ }
194
+
195
+ &--horizontal .sign-up-form__fieldset__group {
196
+ flex-direction: row;
197
+ }
198
+
199
+ &--compact {
200
+ .sign-up-form__fieldset__label {
201
+ margin-bottom: 0;
202
+ }
203
+
204
+ .sign-up-form__fieldset__group {
205
+ gap: 0;
206
+ }
207
+ }
208
+
173
209
  .sign-up-form__fieldset__group__addon.btn {
174
210
  font-size: 0.9em;
175
211
  }
@@ -1,9 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import get from 'lodash/get'
3
3
  import reduce from 'lodash/reduce'
4
- import uniqueId from 'lodash/uniqueId'
5
4
  import {
6
5
  computed,
6
+ ref,
7
7
  type CSSProperties
8
8
  } from 'vue'
9
9
 
@@ -12,7 +12,7 @@ import AppIcon from '@/components/App/AppIcon.vue'
12
12
  import SharingOptionsLink from '@/components/SharingOptions/SharingOptionsLink.vue'
13
13
  import config from '@/config'
14
14
  import IframeResizer from '@/utils/iframe-resizer'
15
- import { BModal, useModal } from 'bootstrap-vue-next'
15
+ import { BModal } from 'bootstrap-vue-next'
16
16
 
17
17
  interface MetaValuesMap {
18
18
  url: string
@@ -21,8 +21,6 @@ interface MetaValuesMap {
21
21
  facebook_title: string
22
22
  facebook_description: string
23
23
  facebook_media: string
24
- twitter_media: string
25
- twitter_user: string
26
24
  }
27
25
 
28
26
  export interface SharingOptionsProps {
@@ -77,8 +75,10 @@ const props = withDefaults(defineProps<SharingOptionsProps>(), {
77
75
  noMeta: false
78
76
  })
79
77
 
80
- const embedFormId = uniqueId('embed-form-')
81
- const { show } = useModal(embedFormId)
78
+ const showEmbedForm = ref(false)
79
+ const show = () => {
80
+ showEmbedForm.value = true
81
+ }
82
82
  const style = computed((): CSSProperties => {
83
83
  return {
84
84
  'flex-direction': props.direction
@@ -105,14 +105,6 @@ const metaValues = computed((): MetaValuesMap => {
105
105
  'sharing-options.media',
106
106
  'meta[property="og:image"]'
107
107
  ),
108
- twitter_media: defaultValueFor(
109
- 'sharing-options.media',
110
- 'meta[name="twitter:image"]'
111
- ),
112
- twitter_user: defaultValueFor(
113
- 'sharing-options.twitter-user',
114
- 'meta[name="twitter:site"]'
115
- )
116
108
  }
117
109
  })
118
110
 
@@ -152,8 +144,8 @@ function defaultValueFor(key: string, metaSelector?: string): string {
152
144
  />
153
145
  <sharing-options-link
154
146
  class="sharing-options__link"
155
- network="twitter"
156
- v-bind="valuesFor('twitter')"
147
+ network="bluesky"
148
+ v-bind="valuesFor('bluesky')"
157
149
  />
158
150
  <sharing-options-link
159
151
  class="sharing-options__link"
@@ -170,13 +162,15 @@ function defaultValueFor(key: string, metaSelector?: string): string {
170
162
  class="sharing-options__link sharing-options__link--embed"
171
163
  @click="show"
172
164
  >
173
- <app-icon><i-ph-code /></app-icon>
165
+ <app-icon>
166
+ <i-ph-code-bold />
167
+ </app-icon>
174
168
  <span class="visually-hidden">Embed</span>
175
169
  </a>
176
170
  <b-modal
177
- :id="embedFormId"
171
+ v-model="showEmbedForm"
178
172
  class="text-dark"
179
- hide-footer
173
+ no-footer
180
174
  title="Embed on your website"
181
175
  >
182
176
  <embed-form
@@ -12,11 +12,16 @@ export const $popup: Popup = {
12
12
  parent: typeof window !== 'undefined' ? window : null
13
13
  }
14
14
 
15
+ import type { Component } from 'vue'
15
16
  import { SharingPlatform } from '@/enums'
17
+ import IPhEnvelope from '~icons/ph/envelope-bold'
18
+ import IPhFacebookLogoFill from '~icons/ph/facebook-logo-fill'
19
+ import IPhLinkedinLogoFill from '~icons/ph/linkedin-logo-fill'
20
+ import IPhButterflyFill from '~icons/ph/butterfly-fill'
16
21
 
17
22
  interface SharingPlatformConfig {
18
23
  base: string
19
- icon: string
24
+ icon: Component
20
25
  args: Record<string, string>
21
26
  }
22
27
 
@@ -27,7 +32,7 @@ type SharingPlatforms = Record<SharingPlatform, SharingPlatformConfig>
27
32
  export const networks: SharingPlatforms = {
28
33
  email: {
29
34
  base: 'mailto:?',
30
- icon: 'envelope',
35
+ icon: IPhEnvelope,
31
36
  args: {
32
37
  subject: 'title',
33
38
  body: 'description'
@@ -35,7 +40,7 @@ export const networks: SharingPlatforms = {
35
40
  },
36
41
  facebook: {
37
42
  base: 'https://www.facebook.com/sharer.php?',
38
- icon: 'facebook-logo',
43
+ icon: IPhFacebookLogoFill,
39
44
  args: {
40
45
  u: 'url',
41
46
  title: 'title',
@@ -45,31 +50,19 @@ export const networks: SharingPlatforms = {
45
50
  },
46
51
  linkedin: {
47
52
  base: 'https://www.linkedin.com/sharing/share-offsite/?',
48
- icon: 'linkedin-logo',
53
+ icon: IPhLinkedinLogoFill,
49
54
  args: {
50
55
  url: 'url',
51
56
  title: 'title',
52
57
  summary: 'description'
53
58
  }
54
59
  },
55
- twitter: {
56
- base: 'https://x.com/intent/tweet?',
57
- icon: 'x-logo',
58
- args: {
59
- url: 'url',
60
- text: 'title',
61
- via: 'user',
62
- hashtags: 'hashtags'
63
- }
64
- },
65
- x: {
66
- base: 'https://x.com/intent/tweet?',
67
- icon: 'x-logo',
60
+ bluesky: {
61
+ base: 'https://bsky.app/intent/compose?',
62
+ icon: IPhButterflyFill,
68
63
  args: {
69
- url: 'url',
70
64
  text: 'title',
71
- via: 'user',
72
- hashtags: 'hashtags'
65
+ url: 'url'
73
66
  }
74
67
  }
75
68
  }
@@ -79,20 +72,9 @@ export const networks: SharingPlatforms = {
79
72
  import querystring from 'querystring-es3'
80
73
  import reduce from 'lodash/reduce'
81
74
  import get from 'lodash/get'
82
- import { computed, reactive, type Component } from 'vue'
75
+ import { computed, reactive } from 'vue'
83
76
 
84
77
  import AppIcon from '@/components/App/AppIcon.vue'
85
- import IPhEnvelopeFill from '~icons/ph/envelope-fill'
86
- import IPhFacebookLogoFill from '~icons/ph/facebook-logo-fill'
87
- import IPhLinkedinLogoFill from '~icons/ph/linkedin-logo-fill'
88
- import IPhXLogoFill from '~icons/ph/x-logo-fill'
89
-
90
- const iconMap: Record<string, Component> = {
91
- 'envelope': IPhEnvelopeFill,
92
- 'facebook-logo': IPhFacebookLogoFill,
93
- 'linkedin-logo': IPhLinkedinLogoFill,
94
- 'x-logo': IPhXLogoFill
95
- }
96
78
 
97
79
  defineOptions({
98
80
  name: 'SharingOptionsLink'
@@ -128,7 +110,7 @@ export interface SharingOptionsLinkProps {
128
110
  */
129
111
  media?: string | null
130
112
  /**
131
- * Twitter user
113
+ * Social media user handle
132
114
  */
133
115
  user?: string | null
134
116
  /**
@@ -176,12 +158,8 @@ const args = computed((): Record<string, string> => {
176
158
  return get(networks, [props.network, 'args'], {})
177
159
  })
178
160
 
179
- const iconName = computed((): string | null => {
180
- return get(networks, [props.network, 'icon'], null)
181
- })
182
-
183
161
  const iconComponent = computed((): Component | null => {
184
- return iconName.value ? iconMap[iconName.value] ?? null : null
162
+ return get(networks, [props.network, 'icon'], null)
185
163
  })
186
164
 
187
165
  const query = computed((): Record<string, string> => {
@@ -268,7 +246,10 @@ defineExpose({
268
246
  @click="handleClick"
269
247
  >
270
248
  <slot>
271
- <app-icon v-if="!noIcon && iconComponent">
249
+ <app-icon
250
+ v-if="!noIcon && iconComponent"
251
+ size="1.2em"
252
+ >
272
253
  <component :is="iconComponent" />
273
254
  </app-icon>
274
255
  <span class="visually-hidden">{{ name }}</span>
@@ -26,7 +26,6 @@ export default {
26
26
  'sharing-options.title': 'Awesome App by ICIJ',
27
27
  'sharing-options.description': 'null',
28
28
  'sharing-options.media': null,
29
- 'sharing-options.twitter-user': 'ICIJorg',
30
29
  'signup-form.tracker': 'EXTERNAL',
31
30
  'signup-form.action':
32
31
  'https://icij.us15.list-manage.com/subscribe/post?u=0d48a33b1c24d257734cc2a79&id=992ecfdbb2',
@@ -150,6 +150,7 @@ const emit = defineEmits<{
150
150
 
151
151
  const width = ref(0)
152
152
  const height = ref(0)
153
+ const leftAxisRedrawCount = ref(0)
153
154
  const leftAxisHeight = ref(0)
154
155
  const highlightedKeys = ref(props.highlights)
155
156
  const highlightTimeout = ref<ReturnType<typeof setTimeout> | undefined>(undefined)
@@ -214,6 +215,8 @@ const leftAxis = computed(() => {
214
215
  })
215
216
 
216
217
  const leftAxisLabelsWidth = computed(() => {
218
+ // Track redraw counter to re-measure after axis text is created in the DOM
219
+ void leftAxisRedrawCount.value
217
220
  const selector = '.stacked-column-chart__left-axis__canvas .tick text'
218
221
  const defaultWidth = 0
219
222
  return (
@@ -477,6 +480,12 @@ watch(sortedData, async () => {
477
480
  // Update the left axis only if the bars exists
478
481
  if (element) {
479
482
  leftAxisHeight.value = element.offsetHeight
483
+ // First draw creates tick text in the DOM
484
+ leftAxisCanvas.value.call(leftAxis.value as any)
485
+ // Invalidate leftAxisLabelsWidth so it re-measures the new tick text
486
+ leftAxisRedrawCount.value++
487
+ await nextTick()
488
+ // Second draw uses the correct tickSize and margins
480
489
  leftAxisCanvas.value.call(leftAxis.value as any)
481
490
  }
482
491
  // Compute label overflow/pushed/hidden states after DOM layout
@@ -517,11 +526,11 @@ watch(sortedData, async () => {
517
526
  <span class="stacked-column-chart__legend__item__label">{{ groupName(key) }}</span>
518
527
  </li>
519
528
  </ul>
520
- <div class="d-flex flex-grow-1 position-relative overflow-hidden">
529
+ <div class="stacked-column-chart__chart d-flex flex-column flex-grow-1 position-relative">
521
530
  <svg
522
531
  v-show="noDirectLabeling"
523
532
  :width="width + 'px'"
524
- :height="height + 'px'"
533
+ :height="(leftAxisHeight + 20) + 'px'"
525
534
  class="stacked-column-chart__left-axis"
526
535
  >
527
536
  <g
@@ -530,7 +539,7 @@ watch(sortedData, async () => {
530
539
  />
531
540
  </svg>
532
541
  <div
533
- class="stacked-column-chart__groups d-flex flex-grow-1"
542
+ class="stacked-column-chart__groups d-flex flex-grow-1 overflow-hidden"
534
543
  :style="paddedStyle"
535
544
  >
536
545
  <div
@@ -577,9 +586,18 @@ watch(sortedData, async () => {
577
586
  </teleport>
578
587
  </div>
579
588
  </div>
580
- <div class="stacked-column-chart__groups__item__label small py-2">
581
- {{ formatXDatum(datum[labelField]) }}
582
- </div>
589
+ </div>
590
+ </div>
591
+ <div
592
+ class="stacked-column-chart__labels d-flex"
593
+ :style="paddedStyle"
594
+ >
595
+ <div
596
+ v-for="(datum, i) in sortedData"
597
+ :key="i"
598
+ class="stacked-column-chart__groups__item__label flex-grow-1 small py-2 text-center"
599
+ >
600
+ {{ formatXDatum(datum[labelField]) }}
583
601
  </div>
584
602
  </div>
585
603
  </div>
package/lib/enums.ts CHANGED
@@ -80,8 +80,7 @@ export enum SharingPlatform {
80
80
  email = 'email',
81
81
  facebook = 'facebook',
82
82
  linkedin = 'linkedin',
83
- twitter = 'twitter',
84
- x = 'x'
83
+ bluesky = 'bluesky'
85
84
  }
86
85
 
87
86
  export enum DonatePeriod {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icij/murmur-next",
3
- "version": "4.8.2",
3
+ "version": "4.8.4",
4
4
  "private": false,
5
5
  "description": "Murmur is ICIJ's Design System for Bootstrap 5 and Vue.js",
6
6
  "author": "promera@icij.org",