@growth-labs/cms 0.5.7 → 0.5.8

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 (50) hide show
  1. package/README.md +5 -0
  2. package/dist/integration/options.d.ts +26 -0
  3. package/dist/integration/options.d.ts.map +1 -1
  4. package/dist/integration/options.js +5 -0
  5. package/dist/integration/options.js.map +1 -1
  6. package/dist/routes/content.d.ts.map +1 -1
  7. package/dist/routes/content.js +6 -1
  8. package/dist/routes/content.js.map +1 -1
  9. package/dist/ui/client/mount.d.ts.map +1 -1
  10. package/dist/ui/client/mount.js +1 -0
  11. package/dist/ui/client/mount.js.map +1 -1
  12. package/dist/ui/editor/ContentForm.d.ts +5 -1
  13. package/dist/ui/editor/ContentForm.d.ts.map +1 -1
  14. package/dist/ui/editor/ContentForm.js +8 -2
  15. package/dist/ui/editor/ContentForm.js.map +1 -1
  16. package/dist/ui/editor/content-payload.d.ts +1 -0
  17. package/dist/ui/editor/content-payload.d.ts.map +1 -1
  18. package/dist/ui/editor/content-payload.js +6 -1
  19. package/dist/ui/editor/content-payload.js.map +1 -1
  20. package/dist/ui/screens/ContentRoute.d.ts +5 -1
  21. package/dist/ui/screens/ContentRoute.d.ts.map +1 -1
  22. package/dist/ui/screens/ContentRoute.js +3 -3
  23. package/dist/ui/screens/ContentRoute.js.map +1 -1
  24. package/dist/ui/screens/EditorScreen.d.ts +5 -1
  25. package/dist/ui/screens/EditorScreen.d.ts.map +1 -1
  26. package/dist/ui/screens/EditorScreen.js +2 -2
  27. package/dist/ui/screens/EditorScreen.js.map +1 -1
  28. package/dist/ui/screens/LibraryScreen.d.ts +5 -1
  29. package/dist/ui/screens/LibraryScreen.d.ts.map +1 -1
  30. package/dist/ui/screens/LibraryScreen.js +20 -14
  31. package/dist/ui/screens/LibraryScreen.js.map +1 -1
  32. package/dist/ui/screens/library-data.d.ts +6 -0
  33. package/dist/ui/screens/library-data.d.ts.map +1 -1
  34. package/dist/ui/screens/library-data.js +8 -1
  35. package/dist/ui/screens/library-data.js.map +1 -1
  36. package/dist/ui/screens/registry.d.ts +5 -1
  37. package/dist/ui/screens/registry.d.ts.map +1 -1
  38. package/dist/ui/screens/registry.js +2 -2
  39. package/dist/ui/screens/registry.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/integration/options.ts +6 -0
  42. package/src/routes/content.ts +7 -1
  43. package/src/ui/client/mount.tsx +1 -0
  44. package/src/ui/editor/ContentForm.tsx +30 -0
  45. package/src/ui/editor/content-payload.ts +6 -1
  46. package/src/ui/screens/ContentRoute.tsx +10 -1
  47. package/src/ui/screens/EditorScreen.tsx +3 -0
  48. package/src/ui/screens/LibraryScreen.tsx +76 -6
  49. package/src/ui/screens/library-data.ts +12 -1
  50. package/src/ui/screens/registry.tsx +8 -1
@@ -20,6 +20,7 @@ import {
20
20
  filterRows,
21
21
  groupByStatus,
22
22
  type LibraryRow,
23
+ primaryCategoryLabel,
23
24
  type StatusCounts,
24
25
  } from './library-data.js'
25
26
 
@@ -30,6 +31,7 @@ import {
30
31
  export interface LibraryScreenProps {
31
32
  openDoc: (id: string | null, contentType?: ContentType) => void
32
33
  allowInlineAuthorCreation?: boolean
34
+ primaryCategories?: Array<{ slug: string; label: string }>
33
35
  }
34
36
 
35
37
  // ---------------------------------------------------------------------------
@@ -74,6 +76,7 @@ interface LibraryState {
74
76
  /** The currently active status tab filter. */
75
77
  statusFilter: ContentStatus | 'all' | 'processing'
76
78
  typeFilter: ContentType | ''
79
+ channelFilter: string
77
80
  q: string
78
81
  view: 'list' | 'board'
79
82
  /** Row IDs currently selected. */
@@ -93,6 +96,7 @@ type LibraryAction =
93
96
  | { type: 'fetch-error'; error: string }
94
97
  | { type: 'set-status-filter'; status: ContentStatus | 'all' | 'processing' }
95
98
  | { type: 'set-type-filter'; contentType: ContentType | '' }
99
+ | { type: 'set-channel-filter'; channel: string }
96
100
  | { type: 'set-q'; q: string }
97
101
  | { type: 'set-view'; view: 'list' | 'board' }
98
102
  | { type: 'toggle-select'; id: string }
@@ -112,6 +116,7 @@ function initLibrary(): LibraryState {
112
116
  error: null,
113
117
  statusFilter: 'all',
114
118
  typeFilter: '',
119
+ channelFilter: '',
115
120
  q: '',
116
121
  view: 'list',
117
122
  selected: new Set(),
@@ -141,6 +146,8 @@ function libraryReducer(state: LibraryState, action: LibraryAction): LibraryStat
141
146
  return { ...state, statusFilter: action.status, selected: new Set() }
142
147
  case 'set-type-filter':
143
148
  return { ...state, typeFilter: action.contentType, selected: new Set() }
149
+ case 'set-channel-filter':
150
+ return { ...state, channelFilter: action.channel, selected: new Set() }
144
151
  case 'set-q':
145
152
  return { ...state, q: action.q }
146
153
  case 'set-view':
@@ -191,6 +198,7 @@ function getVisibleRows(state: LibraryState): LibraryRow[] {
191
198
  ? (state.statusFilter as ContentStatus)
192
199
  : undefined,
193
200
  type: state.typeFilter !== '' ? state.typeFilter : undefined,
201
+ channel: state.channelFilter || undefined,
194
202
  q: state.q || undefined,
195
203
  })
196
204
  }
@@ -199,7 +207,11 @@ function getVisibleRows(state: LibraryState): LibraryRow[] {
199
207
  // Main component
200
208
  // ---------------------------------------------------------------------------
201
209
 
202
- export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: LibraryScreenProps) {
210
+ export function LibraryScreen({
211
+ openDoc,
212
+ allowInlineAuthorCreation = true,
213
+ primaryCategories = [],
214
+ }: LibraryScreenProps) {
203
215
  const { state: ws } = useWorkspace()
204
216
  const siteBase = typeof window !== 'undefined' ? window.location.origin : undefined
205
217
  const [lib, dispatch] = useReducer(libraryReducer, undefined, initLibrary)
@@ -217,6 +229,7 @@ export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: Lib
217
229
  params.set('status', lib.statusFilter)
218
230
  }
219
231
  if (lib.typeFilter) params.set('types', lib.typeFilter)
232
+ if (lib.channelFilter) params.set('channel', lib.channelFilter)
220
233
  if (lib.q) params.set('q', lib.q)
221
234
 
222
235
  const [listRes, countsRes] = await Promise.all([
@@ -247,7 +260,7 @@ export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: Lib
247
260
  } catch (err) {
248
261
  dispatch({ type: 'fetch-error', error: String(err) })
249
262
  }
250
- }, [lib.statusFilter, lib.typeFilter, lib.q]) // fetchContent identity changes when filters change → useEffect re-runs
263
+ }, [lib.statusFilter, lib.typeFilter, lib.channelFilter, lib.q]) // fetchContent identity changes when filters change → useEffect re-runs
251
264
 
252
265
  // Initial fetch + refetch when filters change
253
266
  useEffect(() => {
@@ -373,6 +386,24 @@ export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: Lib
373
386
  ))}
374
387
  </div>
375
388
 
389
+ {primaryCategories.length > 0 && (
390
+ <select
391
+ value={lib.channelFilter}
392
+ onChange={(event) =>
393
+ dispatch({ type: 'set-channel-filter', channel: event.currentTarget.value })
394
+ }
395
+ aria-label="Content grouping"
396
+ className="input"
397
+ >
398
+ <option value="">All groupings</option>
399
+ {primaryCategories.map((category) => (
400
+ <option key={category.slug} value={category.slug}>
401
+ {category.label}
402
+ </option>
403
+ ))}
404
+ </select>
405
+ )}
406
+
376
407
  <div style={{ flex: 1 }} />
377
408
 
378
409
  {/* View toggle — segmented control */}
@@ -457,6 +488,7 @@ export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: Lib
457
488
  (lib.view === 'list' ? (
458
489
  <ListView
459
490
  rows={visible}
491
+ primaryCategories={primaryCategories}
460
492
  selected={lib.selected}
461
493
  allSelected={allVisibleSelected}
462
494
  onToggleSelect={(id) => dispatch({ type: 'toggle-select', id })}
@@ -469,7 +501,11 @@ export function LibraryScreen({ openDoc, allowInlineAuthorCreation = true }: Lib
469
501
  onShare={(row) => dispatch({ type: 'open-share', row })}
470
502
  />
471
503
  ) : (
472
- <BoardView rows={visible} onOpen={(row) => openDoc(row.id, row.type)} />
504
+ <BoardView
505
+ rows={visible}
506
+ primaryCategories={primaryCategories}
507
+ onOpen={(row) => openDoc(row.id, row.type)}
508
+ />
473
509
  ))}
474
510
 
475
511
  {/* Import modal */}
@@ -578,6 +614,7 @@ function NewContentMenu({ openDoc }: { openDoc: LibraryScreenProps['openDoc'] })
578
614
 
579
615
  function ListView({
580
616
  rows,
617
+ primaryCategories,
581
618
  selected,
582
619
  allSelected,
583
620
  onToggleSelect,
@@ -586,6 +623,7 @@ function ListView({
586
623
  onShare,
587
624
  }: {
588
625
  rows: LibraryRow[]
626
+ primaryCategories: Array<{ slug: string; label: string }>
589
627
  selected: Set<string>
590
628
  allSelected: boolean
591
629
  onToggleSelect: (id: string) => void
@@ -609,6 +647,7 @@ function ListView({
609
647
  </th>
610
648
  <th>Title</th>
611
649
  <th style={{ width: 110 }}>Type</th>
650
+ <th style={{ width: 120 }}>Grouping</th>
612
651
  <th style={{ width: 120 }}>Status</th>
613
652
  <th style={{ width: 150 }}>Author</th>
614
653
  <th style={{ width: 130 }}>Updated</th>
@@ -620,6 +659,7 @@ function ListView({
620
659
  <ListRow
621
660
  key={row.id}
622
661
  row={row}
662
+ primaryCategories={primaryCategories}
623
663
  isSelected={selected.has(row.id)}
624
664
  onSelect={() => onToggleSelect(row.id)}
625
665
  onOpen={() => onOpen(row)}
@@ -635,12 +675,14 @@ function ListView({
635
675
 
636
676
  function ListRow({
637
677
  row,
678
+ primaryCategories,
638
679
  isSelected,
639
680
  onSelect,
640
681
  onOpen,
641
682
  onShare,
642
683
  }: {
643
684
  row: LibraryRow
685
+ primaryCategories: Array<{ slug: string; label: string }>
644
686
  isSelected: boolean
645
687
  onSelect: () => void
646
688
  onOpen: () => void
@@ -742,6 +784,10 @@ function ListRow({
742
784
  </span>
743
785
  </td>
744
786
 
787
+ <td style={{ fontSize: 12.5, color: 'var(--ink-soft)' }}>
788
+ {primaryCategoryLabel(row.channel, primaryCategories)}
789
+ </td>
790
+
745
791
  <td>
746
792
  <StatusPill row={row} />
747
793
  </td>
@@ -839,7 +885,15 @@ const BOARD_PILL: Record<(typeof BOARD_COLUMNS)[number]['key'], string> = {
839
885
  published: 'published',
840
886
  }
841
887
 
842
- function BoardView({ rows, onOpen }: { rows: LibraryRow[]; onOpen: (row: LibraryRow) => void }) {
888
+ function BoardView({
889
+ rows,
890
+ primaryCategories,
891
+ onOpen,
892
+ }: {
893
+ rows: LibraryRow[]
894
+ primaryCategories: Array<{ slug: string; label: string }>
895
+ onOpen: (row: LibraryRow) => void
896
+ }) {
843
897
  const columns = groupByStatus(rows)
844
898
 
845
899
  return (
@@ -888,7 +942,12 @@ function BoardView({ rows, onOpen }: { rows: LibraryRow[]; onOpen: (row: Library
888
942
  </div>
889
943
  )}
890
944
  {colRows.map((row) => (
891
- <BoardCard key={row.id} row={row} onOpen={() => onOpen(row)} />
945
+ <BoardCard
946
+ key={row.id}
947
+ row={row}
948
+ primaryCategories={primaryCategories}
949
+ onOpen={() => onOpen(row)}
950
+ />
892
951
  ))}
893
952
  </div>
894
953
  </div>
@@ -898,7 +957,15 @@ function BoardView({ rows, onOpen }: { rows: LibraryRow[]; onOpen: (row: Library
898
957
  )
899
958
  }
900
959
 
901
- function BoardCard({ row, onOpen }: { row: LibraryRow; onOpen: () => void }) {
960
+ function BoardCard({
961
+ row,
962
+ primaryCategories,
963
+ onOpen,
964
+ }: {
965
+ row: LibraryRow
966
+ primaryCategories: Array<{ slug: string; label: string }>
967
+ onOpen: () => void
968
+ }) {
902
969
  const isProcessing =
903
970
  row.processingState != null && !['ready', 'failed'].includes(row.processingState ?? '')
904
971
 
@@ -990,6 +1057,9 @@ function BoardCard({ row, onOpen }: { row: LibraryRow; onOpen: () => void }) {
990
1057
  {row.title || '(Untitled)'}
991
1058
  </div>
992
1059
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1060
+ <span style={{ fontSize: 11.5, color: 'var(--ink-muted)' }}>
1061
+ {primaryCategoryLabel(row.channel, primaryCategories)}
1062
+ </span>
993
1063
  <span style={{ fontSize: 11.5, color: 'var(--ink-muted)' }}>
994
1064
  {row.updatedAt ? formatDate(row.updatedAt) : (row.authorName ?? '—')}
995
1065
  </span>
@@ -25,6 +25,7 @@ export interface LibraryRow {
25
25
  status: ContentStatus
26
26
  slug: string
27
27
  title: string
28
+ channel: string | null
28
29
  visibility: 'free' | 'premium'
29
30
  featured: boolean
30
31
  publishAt: string | null
@@ -62,10 +63,19 @@ export interface BoardColumns {
62
63
  export interface RowFilter {
63
64
  status?: ContentStatus
64
65
  type?: ContentType
66
+ channel?: string
65
67
  /** Case-insensitive substring match against title and slug. */
66
68
  q?: string
67
69
  }
68
70
 
71
+ export function primaryCategoryLabel(
72
+ channel: string | null,
73
+ categories: Array<{ slug: string; label: string }>,
74
+ ): string {
75
+ if (!channel) return '—'
76
+ return categories.find((category) => category.slug === channel)?.label ?? channel
77
+ }
78
+
69
79
  const NON_PROCESSING_STATES = new Set(['ready', 'failed'])
70
80
 
71
81
  /** Returns true when this row represents a video mid-flight in Foundry. */
@@ -161,12 +171,13 @@ export function groupByStatus(rows: LibraryRow[]): BoardColumns {
161
171
  * matching the server-side LIKE behaviour so the list result is consistent.
162
172
  */
163
173
  export function filterRows(rows: LibraryRow[], filter: RowFilter): LibraryRow[] {
164
- const { status, type, q } = filter
174
+ const { status, type, channel, q } = filter
165
175
  const lowerQ = q ? q.toLowerCase() : null
166
176
 
167
177
  return rows.filter((row) => {
168
178
  if (status !== undefined && row.status !== status) return false
169
179
  if (type !== undefined && row.type !== type) return false
180
+ if (channel !== undefined && row.channel !== channel) return false
170
181
  if (lowerQ !== null) {
171
182
  const titleMatch = row.title.toLowerCase().includes(lowerQ)
172
183
  const slugMatch = row.slug.toLowerCase().includes(lowerQ)
@@ -41,13 +41,20 @@ function CalendarRoute() {
41
41
 
42
42
  export function createCmsScreens({
43
43
  allowInlineAuthorCreation = true,
44
+ primaryCategories = [],
44
45
  surveyId,
45
46
  }: {
46
47
  allowInlineAuthorCreation?: boolean
48
+ primaryCategories?: Array<{ slug: string; label: string }>
47
49
  surveyId?: string
48
50
  } = {}): ScreenRegistry {
49
51
  return {
50
- content: () => <ContentRoute allowInlineAuthorCreation={allowInlineAuthorCreation} />,
52
+ content: () => (
53
+ <ContentRoute
54
+ allowInlineAuthorCreation={allowInlineAuthorCreation}
55
+ primaryCategories={primaryCategories}
56
+ />
57
+ ),
51
58
  'content-insights': () => <ContentInsightsScreen />,
52
59
  dashboard: () => <DashboardRoute />,
53
60
  calendar: () => <CalendarRoute />,