@growth-labs/cms 0.5.4 → 0.5.5

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 (69) hide show
  1. package/README.md +19 -1
  2. package/dist/integration/options.d.ts +19 -0
  3. package/dist/integration/options.d.ts.map +1 -1
  4. package/dist/integration/options.js +6 -0
  5. package/dist/integration/options.js.map +1 -1
  6. package/dist/routes/authors.d.ts.map +1 -1
  7. package/dist/routes/authors.js +33 -8
  8. package/dist/routes/authors.js.map +1 -1
  9. package/dist/routes/recovery-errors.d.ts +21 -0
  10. package/dist/routes/recovery-errors.d.ts.map +1 -0
  11. package/dist/routes/recovery-errors.js +53 -0
  12. package/dist/routes/recovery-errors.js.map +1 -0
  13. package/dist/ui/client/mount.d.ts.map +1 -1
  14. package/dist/ui/client/mount.js +5 -2
  15. package/dist/ui/client/mount.js.map +1 -1
  16. package/dist/ui/components/ShareModal.d.ts +4 -1
  17. package/dist/ui/components/ShareModal.d.ts.map +1 -1
  18. package/dist/ui/components/ShareModal.js +39 -9
  19. package/dist/ui/components/ShareModal.js.map +1 -1
  20. package/dist/ui/components/SharePickers.d.ts +29 -2
  21. package/dist/ui/components/SharePickers.d.ts.map +1 -1
  22. package/dist/ui/components/SharePickers.js +33 -8
  23. package/dist/ui/components/SharePickers.js.map +1 -1
  24. package/dist/ui/screens/AuthorsScreen.d.ts.map +1 -1
  25. package/dist/ui/screens/AuthorsScreen.js +2 -1
  26. package/dist/ui/screens/AuthorsScreen.js.map +1 -1
  27. package/dist/ui/screens/ContentRoute.d.ts +3 -1
  28. package/dist/ui/screens/ContentRoute.d.ts.map +1 -1
  29. package/dist/ui/screens/ContentRoute.js +3 -3
  30. package/dist/ui/screens/ContentRoute.js.map +1 -1
  31. package/dist/ui/screens/EditorScreen.d.ts +2 -1
  32. package/dist/ui/screens/EditorScreen.d.ts.map +1 -1
  33. package/dist/ui/screens/EditorScreen.js +2 -2
  34. package/dist/ui/screens/EditorScreen.js.map +1 -1
  35. package/dist/ui/screens/LibraryScreen.d.ts +2 -1
  36. package/dist/ui/screens/LibraryScreen.d.ts.map +1 -1
  37. package/dist/ui/screens/LibraryScreen.js +2 -2
  38. package/dist/ui/screens/LibraryScreen.js.map +1 -1
  39. package/dist/ui/screens/SocialShareScreen.d.ts +3 -1
  40. package/dist/ui/screens/SocialShareScreen.d.ts.map +1 -1
  41. package/dist/ui/screens/SocialShareScreen.js +20 -8
  42. package/dist/ui/screens/SocialShareScreen.js.map +1 -1
  43. package/dist/ui/screens/author-recovery.d.ts +11 -0
  44. package/dist/ui/screens/author-recovery.d.ts.map +1 -0
  45. package/dist/ui/screens/author-recovery.js +18 -0
  46. package/dist/ui/screens/author-recovery.js.map +1 -0
  47. package/dist/ui/screens/registry.d.ts +3 -0
  48. package/dist/ui/screens/registry.d.ts.map +1 -1
  49. package/dist/ui/screens/registry.js +16 -13
  50. package/dist/ui/screens/registry.js.map +1 -1
  51. package/dist/ui/screens/social-share-data.d.ts +23 -2
  52. package/dist/ui/screens/social-share-data.d.ts.map +1 -1
  53. package/dist/ui/screens/social-share-data.js +57 -11
  54. package/dist/ui/screens/social-share-data.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/integration/options.ts +7 -0
  57. package/src/routes/authors.ts +41 -20
  58. package/src/routes/recovery-errors.ts +70 -0
  59. package/src/ui/client/mount.tsx +5 -2
  60. package/src/ui/components/ShareModal.tsx +102 -19
  61. package/src/ui/components/SharePickers.tsx +76 -27
  62. package/src/ui/screens/AuthorsScreen.tsx +7 -2
  63. package/src/ui/screens/ContentRoute.tsx +13 -3
  64. package/src/ui/screens/EditorScreen.tsx +7 -1
  65. package/src/ui/screens/LibraryScreen.tsx +3 -1
  66. package/src/ui/screens/SocialShareScreen.tsx +53 -6
  67. package/src/ui/screens/author-recovery.ts +32 -0
  68. package/src/ui/screens/registry.tsx +22 -12
  69. package/src/ui/screens/social-share-data.ts +92 -13
@@ -22,6 +22,13 @@ export const SHARE_MODES = ['author-social', 'owned-social', 'youtube', 'newslet
22
22
 
23
23
  export type ShareMode = (typeof SHARE_MODES)[number]
24
24
 
25
+ export function requiresCanonicalAuthorRoster(
26
+ mode: ShareMode,
27
+ allowInlineAuthorCreation: boolean,
28
+ ): boolean {
29
+ return mode === 'author-social' && !allowInlineAuthorCreation
30
+ }
31
+
25
32
  /**
26
33
  * Social platform options.
27
34
  * SOURCE OF TRUTH: @growth-labs/analytics src/utils/social.ts SOCIAL_PLATFORMS
@@ -91,6 +98,75 @@ export interface AuthorRef {
91
98
  create?: boolean
92
99
  }
93
100
 
101
+ export interface CanonicalAuthorEntry {
102
+ id: string
103
+ slug: string
104
+ displayName: string
105
+ }
106
+
107
+ export type CanonicalAuthorRosterStatus =
108
+ | { blocked: false; kind: 'not-required' | 'synchronized'; message: null }
109
+ | {
110
+ blocked: true
111
+ kind: 'loading' | 'missing-author' | 'out-of-sync' | 'unavailable'
112
+ message: string
113
+ }
114
+
115
+ export function canonicalAuthorRosterStatus({
116
+ enabled,
117
+ author,
118
+ authors,
119
+ loading,
120
+ error,
121
+ }: {
122
+ enabled: boolean
123
+ author: AuthorRef
124
+ authors: CanonicalAuthorEntry[]
125
+ loading: boolean
126
+ error: string | null
127
+ }): CanonicalAuthorRosterStatus {
128
+ if (!enabled) return { blocked: false, kind: 'not-required', message: null }
129
+
130
+ const id = author.id?.trim() ?? ''
131
+ const slug = author.slug?.trim().toLowerCase() ?? ''
132
+ const identity = id || slug
133
+ if (!identity) {
134
+ return {
135
+ blocked: true,
136
+ kind: 'missing-author',
137
+ message:
138
+ 'No canonical author is linked to this content. Add or publish the author in the Authors screen, then retry.',
139
+ }
140
+ }
141
+ if (loading) {
142
+ return {
143
+ blocked: true,
144
+ kind: 'loading',
145
+ message: `Checking the synchronized author roster for canonical author ${identity}.`,
146
+ }
147
+ }
148
+ if (error) {
149
+ return {
150
+ blocked: true,
151
+ kind: 'unavailable',
152
+ message: `The author roster could not be verified for canonical author ${identity}. Refresh and retry; sharing is blocked until synchronization can be proven.`,
153
+ }
154
+ }
155
+
156
+ const synchronized =
157
+ id !== ''
158
+ ? authors.some((candidate) => candidate.id === id)
159
+ : authors.some((candidate) => candidate.slug.trim().toLowerCase() === slug)
160
+ if (!synchronized) {
161
+ return {
162
+ blocked: true,
163
+ kind: 'out-of-sync',
164
+ message: `Author roster out of sync for canonical author ${identity}. Publish or synchronize that author in the Authors screen, refresh, and retry.`,
165
+ }
166
+ }
167
+ return { blocked: false, kind: 'synchronized', message: null }
168
+ }
169
+
94
170
  /** A reference to a social handle — existing by id/username or inline-created. */
95
171
  export interface HandleRef {
96
172
  id?: string
@@ -175,9 +251,7 @@ export interface ShareFormPrefill {
175
251
  */
176
252
  export function initShareForm(mode: ShareMode, prefill?: ShareFormPrefill): ShareFormState {
177
253
  const defaultCampaign =
178
- mode === 'author-social' || mode === 'owned-social'
179
- ? prefill?.campaign?.trim() || prefill?.contentSlug?.trim() || ''
180
- : ''
254
+ mode === 'owned-social' ? prefill?.campaign?.trim() || prefill?.contentSlug?.trim() || '' : ''
181
255
  const base: ShareFormState = {
182
256
  mode,
183
257
  destinationUrl: prefill?.destinationUrl ?? '',
@@ -281,7 +355,12 @@ export function shareFormReducer(state: ShareFormState, action: ShareFormAction)
281
355
  mode,
282
356
  // Mode-specific resets
283
357
  platform: mode === 'youtube' ? 'youtube' : mode === 'newsletter' ? '' : '',
284
- campaign: mode === 'youtube' || mode === 'newsletter' ? '' : state.campaign,
358
+ campaign:
359
+ mode === 'owned-social'
360
+ ? state.campaign || state.contentSlug
361
+ : mode === 'author-social'
362
+ ? ''
363
+ : '',
285
364
  placements: mode === 'youtube' ? [...YOUTUBE_BUNDLE_PLACEMENTS] : [],
286
365
  youtubeVideoId: mode === 'youtube' ? state.youtubeVideoId : '',
287
366
  issue: mode === 'newsletter' ? state.issue : '',
@@ -289,15 +368,14 @@ export function shareFormReducer(state: ShareFormState, action: ShareFormAction)
289
368
  }
290
369
 
291
370
  case 'prefill-content': {
292
- const shouldDefaultCampaign =
293
- (state.mode === 'author-social' || state.mode === 'owned-social') && !state.campaign.trim()
371
+ const shouldDefaultCampaign = state.mode === 'owned-social' && !state.campaign.trim()
294
372
  return {
295
373
  ...state,
296
374
  destinationUrl: action.destinationUrl,
297
375
  contentSlug: action.contentSlug,
298
376
  contentType: action.contentType,
299
377
  campaign: shouldDefaultCampaign ? action.contentSlug : state.campaign,
300
- ...(action.author ? { author: action.author } : {}),
378
+ author: action.author ?? {},
301
379
  }
302
380
  }
303
381
 
@@ -349,7 +427,6 @@ export interface AuthorSocialBatchPayload {
349
427
  kind: 'author-social'
350
428
  destinationUrl: string
351
429
  platform: string
352
- campaign: string
353
430
  placement?: string
354
431
  boostMode?: string
355
432
  promoCode?: string
@@ -462,14 +539,16 @@ export function buildBatchPayloadFrom(state: ShareFormState): BuildPayloadResult
462
539
 
463
540
  switch (state.mode) {
464
541
  case 'author-social': {
465
- if (!state.campaign.trim()) {
466
- return { ok: false, error: 'Campaign is required for author-social.' }
542
+ if (!refHasContent(state.author)) {
543
+ return {
544
+ ok: false,
545
+ error: 'A canonical author is required for Author Social. Select a synchronized author.',
546
+ }
467
547
  }
468
548
  const payload: AuthorSocialBatchPayload = {
469
549
  kind: 'author-social',
470
550
  ...shared,
471
551
  platform: state.platform,
472
- campaign: state.campaign.trim(),
473
552
  ...(state.placement && state.placement !== 'unknown' ? { placement: state.placement } : {}),
474
553
  ...(state.boostMode && state.boostMode !== 'none' ? { boostMode: state.boostMode } : {}),
475
554
  ...(nonEmpty(state.promoCode) ? { promoCode: state.promoCode } : {}),
@@ -701,7 +780,7 @@ export function toPlainText(output: ShareOutput): string {
701
780
  * given mode. The React form renders only these inputs; the request builders
702
781
  * read only these fields.
703
782
  *
704
- * 'author-social' → campaign, platform, author, destinationUrl, advanced
783
+ * 'author-social' → platform, author, destinationUrl, advanced
705
784
  * 'owned-social' → campaign, platform, handle, (optional author), destinationUrl, advanced
706
785
  * 'youtube' → youtubeVideoId, placements, (optional handle/author), destinationUrl (NO campaign)
707
786
  * 'newsletter' → issue, (optional author/handle), destinationUrl (NO campaign/platform/boostMode)
@@ -711,7 +790,7 @@ export function relevantFields(mode: ShareMode): string[] {
711
790
 
712
791
  switch (mode) {
713
792
  case 'author-social':
714
- return [...shared, 'campaign', 'platform', 'author', 'placement', 'boostMode', 'promoCode']
793
+ return [...shared, 'platform', 'author', 'placement', 'boostMode', 'promoCode']
715
794
 
716
795
  case 'owned-social':
717
796
  return [