@growth-labs/cms 0.5.3 → 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 (76) 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/providers/types.d.ts +0 -3
  7. package/dist/providers/types.d.ts.map +1 -1
  8. package/dist/routes/authors.d.ts.map +1 -1
  9. package/dist/routes/authors.js +33 -8
  10. package/dist/routes/authors.js.map +1 -1
  11. package/dist/routes/recovery-errors.d.ts +21 -0
  12. package/dist/routes/recovery-errors.d.ts.map +1 -0
  13. package/dist/routes/recovery-errors.js +53 -0
  14. package/dist/routes/recovery-errors.js.map +1 -0
  15. package/dist/routes/subscriptions.d.ts.map +1 -1
  16. package/dist/routes/subscriptions.js +11 -5
  17. package/dist/routes/subscriptions.js.map +1 -1
  18. package/dist/ui/client/mount.d.ts.map +1 -1
  19. package/dist/ui/client/mount.js +5 -2
  20. package/dist/ui/client/mount.js.map +1 -1
  21. package/dist/ui/components/ShareModal.d.ts +4 -1
  22. package/dist/ui/components/ShareModal.d.ts.map +1 -1
  23. package/dist/ui/components/ShareModal.js +39 -9
  24. package/dist/ui/components/ShareModal.js.map +1 -1
  25. package/dist/ui/components/SharePickers.d.ts +29 -2
  26. package/dist/ui/components/SharePickers.d.ts.map +1 -1
  27. package/dist/ui/components/SharePickers.js +33 -8
  28. package/dist/ui/components/SharePickers.js.map +1 -1
  29. package/dist/ui/screens/AuthorsScreen.d.ts.map +1 -1
  30. package/dist/ui/screens/AuthorsScreen.js +2 -1
  31. package/dist/ui/screens/AuthorsScreen.js.map +1 -1
  32. package/dist/ui/screens/ContentRoute.d.ts +3 -1
  33. package/dist/ui/screens/ContentRoute.d.ts.map +1 -1
  34. package/dist/ui/screens/ContentRoute.js +3 -3
  35. package/dist/ui/screens/ContentRoute.js.map +1 -1
  36. package/dist/ui/screens/EditorScreen.d.ts +2 -1
  37. package/dist/ui/screens/EditorScreen.d.ts.map +1 -1
  38. package/dist/ui/screens/EditorScreen.js +2 -2
  39. package/dist/ui/screens/EditorScreen.js.map +1 -1
  40. package/dist/ui/screens/LibraryScreen.d.ts +2 -1
  41. package/dist/ui/screens/LibraryScreen.d.ts.map +1 -1
  42. package/dist/ui/screens/LibraryScreen.js +2 -2
  43. package/dist/ui/screens/LibraryScreen.js.map +1 -1
  44. package/dist/ui/screens/SocialShareScreen.d.ts +3 -1
  45. package/dist/ui/screens/SocialShareScreen.d.ts.map +1 -1
  46. package/dist/ui/screens/SocialShareScreen.js +20 -8
  47. package/dist/ui/screens/SocialShareScreen.js.map +1 -1
  48. package/dist/ui/screens/author-recovery.d.ts +11 -0
  49. package/dist/ui/screens/author-recovery.d.ts.map +1 -0
  50. package/dist/ui/screens/author-recovery.js +18 -0
  51. package/dist/ui/screens/author-recovery.js.map +1 -0
  52. package/dist/ui/screens/registry.d.ts +3 -0
  53. package/dist/ui/screens/registry.d.ts.map +1 -1
  54. package/dist/ui/screens/registry.js +16 -13
  55. package/dist/ui/screens/registry.js.map +1 -1
  56. package/dist/ui/screens/social-share-data.d.ts +23 -2
  57. package/dist/ui/screens/social-share-data.d.ts.map +1 -1
  58. package/dist/ui/screens/social-share-data.js +57 -11
  59. package/dist/ui/screens/social-share-data.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/integration/options.ts +7 -0
  62. package/src/providers/types.ts +0 -3
  63. package/src/routes/authors.ts +41 -20
  64. package/src/routes/recovery-errors.ts +70 -0
  65. package/src/routes/subscriptions.ts +50 -30
  66. package/src/ui/client/mount.tsx +5 -2
  67. package/src/ui/components/ShareModal.tsx +102 -19
  68. package/src/ui/components/SharePickers.tsx +76 -27
  69. package/src/ui/screens/AuthorsScreen.tsx +7 -2
  70. package/src/ui/screens/ContentRoute.tsx +13 -3
  71. package/src/ui/screens/EditorScreen.tsx +7 -1
  72. package/src/ui/screens/LibraryScreen.tsx +3 -1
  73. package/src/ui/screens/SocialShareScreen.tsx +53 -6
  74. package/src/ui/screens/author-recovery.ts +32 -0
  75. package/src/ui/screens/registry.tsx +22 -12
  76. package/src/ui/screens/social-share-data.ts +92 -13
@@ -26,10 +26,12 @@ import {
26
26
  BOOST_MODES,
27
27
  type BoostMode,
28
28
  buildBatchPayloadFrom,
29
+ canonicalAuthorRosterStatus,
29
30
  formatShareOutput,
30
31
  initShareForm,
31
32
  type LinkResult,
32
33
  relevantFields,
34
+ requiresCanonicalAuthorRoster,
33
35
  SHARE_MODES,
34
36
  type ShareFormPrefill,
35
37
  type ShareMode,
@@ -41,6 +43,7 @@ import {
41
43
  toPlainText,
42
44
  YOUTUBE_BUNDLE_PLACEMENTS,
43
45
  } from '../screens/social-share-data.js'
46
+ import { type ShareLinksData, useShareLinksData } from './SharePickers.js'
44
47
 
45
48
  // ---------------------------------------------------------------------------
46
49
  // Props
@@ -62,6 +65,8 @@ export interface ShareModalPrefill {
62
65
  export interface ShareModalProps {
63
66
  prefill: ShareModalPrefill
64
67
  onClose: () => void
68
+ allowInlineAuthorCreation?: boolean
69
+ shareLinksData?: ShareLinksData
65
70
  }
66
71
 
67
72
  // ---------------------------------------------------------------------------
@@ -79,7 +84,12 @@ const MODE_LABELS: Record<ShareMode, string> = {
79
84
  // ShareModal
80
85
  // ---------------------------------------------------------------------------
81
86
 
82
- export function ShareModal({ prefill, onClose }: ShareModalProps) {
87
+ export function ShareModal({
88
+ prefill,
89
+ onClose,
90
+ allowInlineAuthorCreation = true,
91
+ shareLinksData: suppliedShareLinksData,
92
+ }: ShareModalProps) {
83
93
  const initialPrefill: ShareFormPrefill = {
84
94
  destinationUrl: prefill.destinationUrl,
85
95
  contentSlug: prefill.contentSlug,
@@ -97,6 +107,23 @@ export function ShareModal({ prefill, onClose }: ShareModalProps) {
97
107
  const [submitError, setSubmitError] = useState<string | null>(null)
98
108
  const [output, setOutput] = useState<ShareOutput | null>(null)
99
109
  const [copied, setCopied] = useState(false)
110
+ const shouldVerifyAuthorRoster = requiresCanonicalAuthorRoster(
111
+ state.mode,
112
+ allowInlineAuthorCreation,
113
+ )
114
+ const fetchedShareLinksData = useShareLinksData(
115
+ !suppliedShareLinksData && !allowInlineAuthorCreation,
116
+ )
117
+ const shareLinksData = suppliedShareLinksData ?? fetchedShareLinksData
118
+ const authorRosterStatus = canonicalAuthorRosterStatus({
119
+ enabled: shouldVerifyAuthorRoster,
120
+ author: state.author,
121
+ authors: shareLinksData.authors,
122
+ loading: shareLinksData.loading,
123
+ error: shareLinksData.error,
124
+ })
125
+ const authorRosterFailure =
126
+ authorRosterStatus.kind === 'out-of-sync' || authorRosterStatus.kind === 'unavailable'
100
127
 
101
128
  const goBase = typeof window !== 'undefined' ? `${window.location.origin}/go` : '/go'
102
129
 
@@ -114,6 +141,11 @@ export function ShareModal({ prefill, onClose }: ShareModalProps) {
114
141
  setSubmitError(null)
115
142
  setOutput(null)
116
143
 
144
+ if (authorRosterStatus.blocked) {
145
+ setSubmitError(authorRosterStatus.message)
146
+ return
147
+ }
148
+
117
149
  const result = buildBatchPayloadFrom(state)
118
150
  if (!result.ok) {
119
151
  setSubmitError(result.error)
@@ -142,7 +174,7 @@ export function ShareModal({ prefill, onClose }: ShareModalProps) {
142
174
  setSubmitting(false)
143
175
  }
144
176
  },
145
- [state, goBase],
177
+ [state, goBase, authorRosterStatus],
146
178
  )
147
179
 
148
180
  const handleCopyAll = useCallback(() => {
@@ -353,22 +385,69 @@ export function ShareModal({ prefill, onClose }: ShareModalProps) {
353
385
  {/* Author ref (inline — simplified for the modal; full picker in the screen) */}
354
386
  {fields.includes('author') && (
355
387
  <div style={fieldGroup}>
356
- <label htmlFor="sm-author" className="field-label">
357
- Author display name
358
- </label>
359
- <input
360
- id="sm-author"
361
- type="text"
362
- className="input"
363
- value={state.author.displayName ?? ''}
364
- onChange={(e) =>
365
- dispatch({
366
- type: 'set-author',
367
- ref: { ...state.author, displayName: e.currentTarget.value, create: true },
368
- })
369
- }
370
- placeholder="Jane Smith"
371
- />
388
+ {allowInlineAuthorCreation ? (
389
+ <>
390
+ <label htmlFor="sm-author" className="field-label">
391
+ Author display name
392
+ </label>
393
+ <input
394
+ id="sm-author"
395
+ type="text"
396
+ className="input"
397
+ value={state.author.displayName ?? ''}
398
+ onChange={(e) =>
399
+ dispatch({
400
+ type: 'set-author',
401
+ ref: {
402
+ ...state.author,
403
+ displayName: e.currentTarget.value,
404
+ create: true,
405
+ },
406
+ })
407
+ }
408
+ placeholder="Jane Smith"
409
+ />
410
+ </>
411
+ ) : authorRosterFailure ? (
412
+ <div
413
+ role="alert"
414
+ style={{
415
+ padding: '10px 12px',
416
+ border: '1px solid color-mix(in srgb, var(--red) 25%, transparent)',
417
+ borderRadius: 'var(--radius-sm)',
418
+ color: 'var(--red)',
419
+ }}
420
+ >
421
+ {authorRosterStatus.message}
422
+ </div>
423
+ ) : state.author.id || state.author.slug || state.author.displayName ? (
424
+ <>
425
+ <div className="field-label">Canonical author</div>
426
+ <div className="input">
427
+ {state.author.displayName || state.author.slug || state.author.id}
428
+ </div>
429
+ {authorRosterStatus.kind === 'loading' && (
430
+ <div className="help" style={{ marginTop: 6 }}>
431
+ {authorRosterStatus.message}
432
+ </div>
433
+ )}
434
+ </>
435
+ ) : state.mode === 'author-social' ? (
436
+ <div
437
+ role="alert"
438
+ style={{
439
+ padding: '10px 12px',
440
+ border: '1px solid color-mix(in srgb, var(--red) 25%, transparent)',
441
+ borderRadius: 'var(--radius-sm)',
442
+ color: 'var(--red)',
443
+ }}
444
+ >
445
+ No canonical author is linked to this content. Add or publish the author in the
446
+ Authors screen, then retry.
447
+ </div>
448
+ ) : (
449
+ <div className="help">No author attribution selected (optional).</div>
450
+ )}
372
451
  </div>
373
452
  )}
374
453
 
@@ -514,7 +593,11 @@ export function ShareModal({ prefill, onClose }: ShareModalProps) {
514
593
  <button type="button" className="btn" onClick={onClose}>
515
594
  Cancel
516
595
  </button>
517
- <button type="submit" className="btn primary" disabled={submitting}>
596
+ <button
597
+ type="submit"
598
+ className="btn primary"
599
+ disabled={submitting || authorRosterStatus.blocked}
600
+ >
518
601
  <Icon name="share" size={14} />
519
602
  {submitting ? 'Minting…' : 'Mint link(s)'}
520
603
  </button>
@@ -27,11 +27,14 @@
27
27
  import { useCallback, useEffect, useRef, useState } from 'react'
28
28
  import { Icon } from '../icons.js'
29
29
  import { publicUrlFor } from '../screens/public-url.js'
30
- import type {
31
- AuthorRef,
32
- HandleRef,
33
- ShareFormAction,
34
- ShareMode,
30
+ import {
31
+ type AuthorRef,
32
+ type CanonicalAuthorEntry,
33
+ canonicalAuthorRosterStatus,
34
+ type HandleRef,
35
+ requiresCanonicalAuthorRoster,
36
+ type ShareFormAction,
37
+ type ShareMode,
35
38
  } from '../screens/social-share-data.js'
36
39
 
37
40
  // ---------------------------------------------------------------------------
@@ -39,11 +42,7 @@ import type {
39
42
  // SOURCE OF TRUTH: @growth-labs/analytics src/routes/share-links.ts GET response
40
43
  // ---------------------------------------------------------------------------
41
44
 
42
- interface AuthorEntry {
43
- id: string
44
- slug: string
45
- displayName: string
46
- }
45
+ export interface AuthorEntry extends CanonicalAuthorEntry {}
47
46
 
48
47
  interface HandleEntry {
49
48
  id: string
@@ -60,6 +59,13 @@ interface ShareLinksListResponse {
60
59
  links: unknown[]
61
60
  }
62
61
 
62
+ export interface ShareLinksData {
63
+ authors: AuthorEntry[]
64
+ handles: HandleEntry[]
65
+ loading: boolean
66
+ error: string | null
67
+ }
68
+
63
69
  // LibraryRow subset — only what the content picker needs.
64
70
  // SOURCE OF TRUTH: packages/cms/src/ui/screens/library-data.ts LibraryRow
65
71
  interface ContentRow {
@@ -322,20 +328,21 @@ export function ContentPicker({ siteBase, dispatch }: ContentPickerProps) {
322
328
  * so the component is self-contained; in practice the two fetches are identical
323
329
  * and the browser deduplicates via HTTP cache / same-tick coalescing).
324
330
  */
325
- function useShareLinksData(): {
326
- authors: AuthorEntry[]
327
- handles: HandleEntry[]
328
- loading: boolean
329
- error: string | null
330
- } {
331
+ export function useShareLinksData(enabled = true): ShareLinksData {
331
332
  const [authors, setAuthors] = useState<AuthorEntry[]>([])
332
333
  const [handles, setHandles] = useState<HandleEntry[]>([])
333
- const [loading, setLoading] = useState(true)
334
+ const [loading, setLoading] = useState(enabled)
334
335
  const [error, setError] = useState<string | null>(null)
335
336
 
336
337
  useEffect(() => {
338
+ if (!enabled) {
339
+ setLoading(false)
340
+ return
341
+ }
337
342
  let cancelled = false
338
343
  async function load() {
344
+ setLoading(true)
345
+ setError(null)
339
346
  try {
340
347
  const res = await fetch('/api/share-links')
341
348
  if (!res.ok) {
@@ -357,7 +364,7 @@ function useShareLinksData(): {
357
364
  return () => {
358
365
  cancelled = true
359
366
  }
360
- }, [])
367
+ }, [enabled])
361
368
 
362
369
  return { authors, handles, loading, error }
363
370
  }
@@ -418,6 +425,8 @@ export interface AuthorPickerProps {
418
425
  ref: AuthorRef
419
426
  dispatch: React.Dispatch<ShareFormAction>
420
427
  mode: ShareMode
428
+ allowInlineAuthorCreation?: boolean
429
+ shareLinksData?: ShareLinksData
421
430
  }
422
431
 
423
432
  /**
@@ -425,8 +434,15 @@ export interface AuthorPickerProps {
425
434
  * Supports inline-create via `create:true` + `displayName`.
426
435
  * Required for author-social; optional for newsletter/youtube.
427
436
  */
428
- export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerProps) {
429
- const { authors, loading, error } = useShareLinksData()
437
+ export function AuthorPicker({
438
+ ref: authorRef,
439
+ dispatch,
440
+ mode,
441
+ allowInlineAuthorCreation = true,
442
+ shareLinksData,
443
+ }: AuthorPickerProps) {
444
+ const fetchedShareLinksData = useShareLinksData(!shareLinksData)
445
+ const { authors, loading, error } = shareLinksData ?? fetchedShareLinksData
430
446
  const isRequired = mode === 'author-social'
431
447
  const [addNew, setAddNew] = useState(false)
432
448
  const [newName, setNewName] = useState('')
@@ -434,7 +450,19 @@ export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerPro
434
450
  // If an existing author ref (with id/slug) is set, show a chip
435
451
  const selectedExisting = authorRef.id
436
452
  ? (authors.find((a) => a.id === authorRef.id) ?? null)
437
- : null
453
+ : authorRef.slug
454
+ ? (authors.find(
455
+ (a) => a.slug.trim().toLowerCase() === authorRef.slug?.trim().toLowerCase(),
456
+ ) ?? null)
457
+ : null
458
+ const rosterStatus = canonicalAuthorRosterStatus({
459
+ enabled: requiresCanonicalAuthorRoster(mode, allowInlineAuthorCreation),
460
+ author: authorRef,
461
+ authors,
462
+ loading,
463
+ error,
464
+ })
465
+ const rosterDrift = rosterStatus.kind === 'out-of-sync' || rosterStatus.kind === 'unavailable'
438
466
 
439
467
  const handleSelectExisting = (author: AuthorEntry) => {
440
468
  dispatch({
@@ -465,8 +493,22 @@ export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerPro
465
493
  <div>
466
494
  <FieldLabel required={isRequired}>{isRequired ? 'Author' : 'Author (optional)'}</FieldLabel>
467
495
 
496
+ {rosterDrift && (
497
+ <div
498
+ role="alert"
499
+ style={{
500
+ padding: '10px 12px',
501
+ border: '1px solid color-mix(in srgb, var(--red) 25%, transparent)',
502
+ borderRadius: 'var(--radius-sm)',
503
+ color: 'var(--red)',
504
+ }}
505
+ >
506
+ {rosterStatus.message}
507
+ </div>
508
+ )}
509
+
468
510
  {/* Show current selection as a chip */}
469
- {displayValue ? (
511
+ {!rosterDrift && displayValue ? (
470
512
  <SelectionChip
471
513
  icon="users"
472
514
  label={displayValue}
@@ -474,7 +516,7 @@ export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerPro
474
516
  onClear={handleClear}
475
517
  clearTitle="Clear author"
476
518
  />
477
- ) : (
519
+ ) : !rosterDrift ? (
478
520
  <>
479
521
  {/* Existing authors list */}
480
522
  {loading && (
@@ -521,12 +563,14 @@ export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerPro
521
563
  className="panel panel-pad help"
522
564
  style={{ margin: '0 0 8px', padding: '10px 12px' }}
523
565
  >
524
- No authors yet — add one below.
566
+ {allowInlineAuthorCreation
567
+ ? 'No authors yet — add one below.'
568
+ : 'No synchronized authors are available. Create and publish the author in the canonical Authors screen, then retry.'}
525
569
  </div>
526
570
  )}
527
571
 
528
572
  {/* Inline add new */}
529
- {addNew ? (
573
+ {allowInlineAuthorCreation && addNew ? (
530
574
  <div style={{ display: 'flex', gap: 8 }}>
531
575
  <input
532
576
  type="text"
@@ -555,7 +599,7 @@ export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerPro
555
599
  <Icon name="x" size={13} />
556
600
  </button>
557
601
  </div>
558
- ) : (
602
+ ) : allowInlineAuthorCreation ? (
559
603
  <button
560
604
  type="button"
561
605
  className="btn sm ghost"
@@ -565,8 +609,13 @@ export function AuthorPicker({ ref: authorRef, dispatch, mode }: AuthorPickerPro
565
609
  <Icon name="plus" size={13} />
566
610
  Add new author
567
611
  </button>
568
- )}
612
+ ) : null}
569
613
  </>
614
+ ) : null}
615
+ {!rosterDrift && rosterStatus.kind === 'loading' && (
616
+ <div className="help" style={{ margin: '6px 0 0' }}>
617
+ {rosterStatus.message}
618
+ </div>
570
619
  )}
571
620
  </div>
572
621
  )
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { useCallback, useEffect, useReducer, useState } from 'react'
24
24
  import { Icon } from '../icons.js'
25
+ import { formatAuthorWriteFailure } from './author-recovery.js'
25
26
 
26
27
  // ---------------------------------------------------------------------------
27
28
  // Data shape — mirrors serializeAuthor() in src/routes/authors.ts
@@ -182,11 +183,15 @@ export function AuthorsScreen() {
182
183
  })
183
184
 
184
185
  if (!res.ok) {
185
- const detail = (await res.json().catch(() => null)) as { error?: string } | null
186
+ const detail = (await res.json().catch(() => null)) as {
187
+ error?: string
188
+ errorClass?: string
189
+ author?: { id?: string; slug?: string }
190
+ } | null
186
191
  setEditor((e) => ({
187
192
  ...e,
188
193
  submitting: false,
189
- error: detail?.error ?? `${res.status} ${res.statusText}`,
194
+ error: formatAuthorWriteFailure(detail, `${res.status} ${res.statusText}`),
190
195
  }))
191
196
  return
192
197
  }
@@ -12,7 +12,11 @@ import { type ContentType, contentViewReducer, initialContentView } from './cont
12
12
  import { EditorScreen } from './EditorScreen.js'
13
13
  import { LibraryScreen } from './LibraryScreen.js'
14
14
 
15
- export function ContentRoute() {
15
+ export function ContentRoute({
16
+ allowInlineAuthorCreation = true,
17
+ }: {
18
+ allowInlineAuthorCreation?: boolean
19
+ }) {
16
20
  const [view, dispatch] = useReducer(contentViewReducer, initialContentView)
17
21
 
18
22
  function openDoc(id: string | null, contentType?: ContentType) {
@@ -28,8 +32,14 @@ export function ContentRoute() {
28
32
  }
29
33
 
30
34
  if (view.open === null) {
31
- return <LibraryScreen openDoc={openDoc} />
35
+ return <LibraryScreen openDoc={openDoc} allowInlineAuthorCreation={allowInlineAuthorCreation} />
32
36
  }
33
37
 
34
- return <EditorScreen docRef={view.open} onClose={handleClose} />
38
+ return (
39
+ <EditorScreen
40
+ docRef={view.open}
41
+ onClose={handleClose}
42
+ allowInlineAuthorCreation={allowInlineAuthorCreation}
43
+ />
44
+ )
35
45
  }
@@ -38,6 +38,7 @@ import type { ContentView } from './content-view.js'
38
38
  export interface EditorScreenProps {
39
39
  docRef: NonNullable<ContentView['open']>
40
40
  onClose: () => void
41
+ allowInlineAuthorCreation?: boolean
41
42
  }
42
43
 
43
44
  // ---------------------------------------------------------------------------
@@ -66,7 +67,11 @@ const STATUS_PILL_COLORS: Record<ContentStatus, string> = {
66
67
  // EditorScreen
67
68
  // ---------------------------------------------------------------------------
68
69
 
69
- export function EditorScreen({ docRef, onClose }: EditorScreenProps) {
70
+ export function EditorScreen({
71
+ docRef,
72
+ onClose,
73
+ allowInlineAuthorCreation = true,
74
+ }: EditorScreenProps) {
70
75
  const contentType: ContentType = (docRef.contentType as ContentType | undefined) ?? 'article'
71
76
  const siteBase = typeof window !== 'undefined' ? window.location.origin : undefined
72
77
 
@@ -397,6 +402,7 @@ export function EditorScreen({ docRef, onClose }: EditorScreenProps) {
397
402
  },
398
403
  siteBase,
399
404
  )}
405
+ allowInlineAuthorCreation={allowInlineAuthorCreation}
400
406
  onClose={() => setShareOpen(false)}
401
407
  />
402
408
  )}
@@ -29,6 +29,7 @@ import {
29
29
 
30
30
  export interface LibraryScreenProps {
31
31
  openDoc: (id: string | null, contentType?: ContentType) => void
32
+ allowInlineAuthorCreation?: boolean
32
33
  }
33
34
 
34
35
  // ---------------------------------------------------------------------------
@@ -198,7 +199,7 @@ function getVisibleRows(state: LibraryState): LibraryRow[] {
198
199
  // Main component
199
200
  // ---------------------------------------------------------------------------
200
201
 
201
- export function LibraryScreen({ openDoc }: LibraryScreenProps) {
202
+ export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: LibraryScreenProps) {
202
203
  const { state: ws } = useWorkspace()
203
204
  const siteBase = typeof window !== 'undefined' ? window.location.origin : undefined
204
205
  const [lib, dispatch] = useReducer(libraryReducer, undefined, initLibrary)
@@ -487,6 +488,7 @@ export function LibraryScreen({ openDoc }: LibraryScreenProps) {
487
488
  {lib.shareRow && (
488
489
  <ShareModal
489
490
  prefill={buildSharePrefill(lib.shareRow, siteBase)}
491
+ allowInlineAuthorCreation={allowInlineAuthorCreation}
490
492
  onClose={() => dispatch({ type: 'close-share' })}
491
493
  />
492
494
  )}
@@ -30,17 +30,25 @@
30
30
  // Registry mount and per-content Share action are Task 9.
31
31
 
32
32
  import { useCallback, useEffect, useReducer, useState } from 'react'
33
- import { AuthorPicker, ContentPicker, HandlePicker } from '../components/SharePickers.js'
33
+ import {
34
+ AuthorPicker,
35
+ ContentPicker,
36
+ HandlePicker,
37
+ type ShareLinksData,
38
+ useShareLinksData,
39
+ } from '../components/SharePickers.js'
34
40
  import { type RecentLink, ShareStatsPanel } from '../components/ShareStatsPanel.js'
35
41
  import { Icon } from '../icons.js'
36
42
  import {
37
43
  BOOST_MODES,
38
44
  type BoostMode,
39
45
  buildBatchPayloadFrom,
46
+ canonicalAuthorRosterStatus,
40
47
  formatShareOutput,
41
48
  initShareForm,
42
49
  type LinkResult,
43
50
  relevantFields,
51
+ requiresCanonicalAuthorRoster,
44
52
  SHARE_MODES,
45
53
  type ShareFormState,
46
54
  type ShareMode,
@@ -67,7 +75,11 @@ const MODE_LABELS: Record<ShareMode, string> = {
67
75
  // Screen component
68
76
  // ---------------------------------------------------------------------------
69
77
 
70
- export function SocialShareScreen() {
78
+ export function SocialShareScreen({
79
+ allowInlineAuthorCreation = true,
80
+ }: {
81
+ allowInlineAuthorCreation?: boolean
82
+ }) {
71
83
  const [state, dispatch] = useReducer(
72
84
  shareFormReducer,
73
85
  { mode: 'author-social' as ShareMode },
@@ -78,6 +90,14 @@ export function SocialShareScreen() {
78
90
  const [submitError, setSubmitError] = useState<string | null>(null)
79
91
  const [output, setOutput] = useState<ShareOutput | null>(null)
80
92
  const [copied, setCopied] = useState(false)
93
+ const shareLinksData = useShareLinksData()
94
+ const authorRosterStatus = canonicalAuthorRosterStatus({
95
+ enabled: requiresCanonicalAuthorRoster(state.mode, allowInlineAuthorCreation),
96
+ author: state.author,
97
+ authors: shareLinksData.authors,
98
+ loading: shareLinksData.loading,
99
+ error: shareLinksData.error,
100
+ })
81
101
 
82
102
  // Recent links — fetched from GET /api/share-links (same endpoint the pickers use).
83
103
  // Used by the ShareStatsPanel for the Repoint affordance.
@@ -124,6 +144,11 @@ export function SocialShareScreen() {
124
144
  setSubmitError(null)
125
145
  setOutput(null)
126
146
 
147
+ if (authorRosterStatus.blocked) {
148
+ setSubmitError(authorRosterStatus.message)
149
+ return
150
+ }
151
+
127
152
  const result = buildBatchPayloadFrom(state)
128
153
  if (!result.ok) {
129
154
  setSubmitError(result.error)
@@ -152,7 +177,7 @@ export function SocialShareScreen() {
152
177
  setSubmitting(false)
153
178
  }
154
179
  },
155
- [state, goBase],
180
+ [state, goBase, authorRosterStatus],
156
181
  )
157
182
 
158
183
  const handleCopyAll = useCallback(() => {
@@ -208,7 +233,13 @@ export function SocialShareScreen() {
208
233
  </div>
209
234
 
210
235
  {/* Per-mode fields */}
211
- <PerModeFields state={state} dispatch={dispatch} fields={fields} />
236
+ <PerModeFields
237
+ state={state}
238
+ dispatch={dispatch}
239
+ fields={fields}
240
+ allowInlineAuthorCreation={allowInlineAuthorCreation}
241
+ shareLinksData={shareLinksData}
242
+ />
212
243
 
213
244
  {/* Advanced disclosure */}
214
245
  <AdvancedSection state={state} dispatch={dispatch} fields={fields} />
@@ -231,7 +262,11 @@ export function SocialShareScreen() {
231
262
 
232
263
  {/* Submit */}
233
264
  <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
234
- <button type="submit" className="btn primary" disabled={submitting}>
265
+ <button
266
+ type="submit"
267
+ className="btn primary"
268
+ disabled={submitting || authorRosterStatus.blocked}
269
+ >
235
270
  <Icon name="share" size={15} />
236
271
  {submitting ? 'Minting…' : 'Mint link(s)'}
237
272
  </button>
@@ -281,10 +316,14 @@ function PerModeFields({
281
316
  state,
282
317
  dispatch,
283
318
  fields,
319
+ allowInlineAuthorCreation,
320
+ shareLinksData,
284
321
  }: {
285
322
  state: ShareFormState
286
323
  dispatch: React.Dispatch<Parameters<typeof shareFormReducer>[1]>
287
324
  fields: string[]
325
+ allowInlineAuthorCreation: boolean
326
+ shareLinksData: ShareLinksData
288
327
  }) {
289
328
  const has = (f: string) => fields.includes(f)
290
329
 
@@ -374,7 +413,15 @@ function PerModeFields({
374
413
  )}
375
414
 
376
415
  {/* author ref — author-social (required), newsletter/youtube (optional) */}
377
- {has('author') && <AuthorPicker ref={state.author} dispatch={dispatch} mode={state.mode} />}
416
+ {has('author') && (
417
+ <AuthorPicker
418
+ ref={state.author}
419
+ dispatch={dispatch}
420
+ mode={state.mode}
421
+ allowInlineAuthorCreation={allowInlineAuthorCreation}
422
+ shareLinksData={shareLinksData}
423
+ />
424
+ )}
378
425
 
379
426
  {/* handle ref — author-social, owned-social, newsletter, youtube */}
380
427
  {has('handle') && <HandlePicker ref={state.handle} dispatch={dispatch} mode={state.mode} />}
@@ -0,0 +1,32 @@
1
+ interface AuthorWriteFailureBody {
2
+ error?: string
3
+ errorClass?: string
4
+ author?: {
5
+ id?: string
6
+ slug?: string
7
+ }
8
+ }
9
+
10
+ export function formatAuthorWriteFailure(
11
+ body: AuthorWriteFailureBody | null,
12
+ httpFallback: string,
13
+ ): string {
14
+ if (!body?.errorClass) return body?.error ?? httpFallback
15
+
16
+ const identity = [
17
+ body.author?.id ? `author id: ${body.author.id}` : '',
18
+ body.author?.slug ? `slug: ${body.author.slug}` : '',
19
+ ].filter(Boolean)
20
+ const message = body.error || 'Author synchronization failed'
21
+ const classifiedMessage = message.includes(body.errorClass)
22
+ ? message
23
+ : `${body.errorClass}: ${message}`
24
+ const containsIdentity =
25
+ !!body.author?.id &&
26
+ !!body.author?.slug &&
27
+ classifiedMessage.includes(body.author.id) &&
28
+ classifiedMessage.includes(body.author.slug)
29
+ return `${classifiedMessage}${
30
+ identity.length > 0 && !containsIdentity ? ` (${identity.join('; ')})` : ''
31
+ }`
32
+ }