@growth-labs/cms 0.5.26 → 0.5.29

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 (89) hide show
  1. package/README.md +20 -0
  2. package/dist/engine/slug.d.ts.map +1 -1
  3. package/dist/engine/slug.js +11 -1
  4. package/dist/engine/slug.js.map +1 -1
  5. package/dist/integration/options.d.ts +196 -0
  6. package/dist/integration/options.d.ts.map +1 -1
  7. package/dist/integration/options.js +62 -0
  8. package/dist/integration/options.js.map +1 -1
  9. package/dist/routes/content.d.ts.map +1 -1
  10. package/dist/routes/content.js +30 -0
  11. package/dist/routes/content.js.map +1 -1
  12. package/dist/ui/client/mount.d.ts +1 -0
  13. package/dist/ui/client/mount.d.ts.map +1 -1
  14. package/dist/ui/client/mount.js +2 -0
  15. package/dist/ui/client/mount.js.map +1 -1
  16. package/dist/ui/components/CmsApp.js +2 -11
  17. package/dist/ui/components/CmsApp.js.map +1 -1
  18. package/dist/ui/components/Sidebar.d.ts.map +1 -1
  19. package/dist/ui/components/Sidebar.js +2 -2
  20. package/dist/ui/components/Sidebar.js.map +1 -1
  21. package/dist/ui/components/Topbar.d.ts.map +1 -1
  22. package/dist/ui/components/Topbar.js +1 -10
  23. package/dist/ui/components/Topbar.js.map +1 -1
  24. package/dist/ui/editor/ContentForm.d.ts +3 -1
  25. package/dist/ui/editor/ContentForm.d.ts.map +1 -1
  26. package/dist/ui/editor/ContentForm.js +42 -16
  27. package/dist/ui/editor/ContentForm.js.map +1 -1
  28. package/dist/ui/editor/Rte.js +3 -3
  29. package/dist/ui/editor/Rte.js.map +1 -1
  30. package/dist/ui/editor/content-payload.d.ts +12 -0
  31. package/dist/ui/editor/content-payload.d.ts.map +1 -1
  32. package/dist/ui/editor/content-payload.js +24 -0
  33. package/dist/ui/editor/content-payload.js.map +1 -1
  34. package/dist/ui/inspector/FoundryTab.js +1 -1
  35. package/dist/ui/inspector/FoundryTab.js.map +1 -1
  36. package/dist/ui/pages/admin.astro +1 -1
  37. package/dist/ui/screens/AuthorsScreen.js +1 -1
  38. package/dist/ui/screens/ContentInsightsScreen.js +1 -1
  39. package/dist/ui/screens/ContentInsightsScreen.js.map +1 -1
  40. package/dist/ui/screens/ContentRoute.d.ts +3 -1
  41. package/dist/ui/screens/ContentRoute.d.ts.map +1 -1
  42. package/dist/ui/screens/ContentRoute.js +2 -2
  43. package/dist/ui/screens/ContentRoute.js.map +1 -1
  44. package/dist/ui/screens/EditorScreen.d.ts +6 -1
  45. package/dist/ui/screens/EditorScreen.d.ts.map +1 -1
  46. package/dist/ui/screens/EditorScreen.js +17 -11
  47. package/dist/ui/screens/EditorScreen.js.map +1 -1
  48. package/dist/ui/screens/LibraryScreen.js +2 -2
  49. package/dist/ui/screens/LibraryScreen.js.map +1 -1
  50. package/dist/ui/screens/SettingsScreen.js +1 -1
  51. package/dist/ui/screens/SubscriptionsScreen.js +2 -2
  52. package/dist/ui/screens/registry.d.ts +3 -1
  53. package/dist/ui/screens/registry.d.ts.map +1 -1
  54. package/dist/ui/screens/registry.js +2 -2
  55. package/dist/ui/screens/registry.js.map +1 -1
  56. package/dist/ui/styles/broadsheet.css +131 -156
  57. package/dist/ui/styles/growth-labs-tokens.css +149 -0
  58. package/dist/ui/theme.d.ts +7 -4
  59. package/dist/ui/theme.d.ts.map +1 -1
  60. package/dist/ui/theme.js +24 -65
  61. package/dist/ui/theme.js.map +1 -1
  62. package/dist/ui/use-tweaks.d.ts.map +1 -1
  63. package/dist/ui/use-tweaks.js +2 -1
  64. package/dist/ui/use-tweaks.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/engine/slug.ts +12 -1
  67. package/src/integration/options.ts +69 -0
  68. package/src/routes/content.ts +37 -0
  69. package/src/ui/client/mount.tsx +2 -0
  70. package/src/ui/components/CmsApp.tsx +3 -20
  71. package/src/ui/components/Sidebar.tsx +5 -11
  72. package/src/ui/components/Topbar.tsx +2 -13
  73. package/src/ui/editor/ContentForm.tsx +81 -13
  74. package/src/ui/editor/Rte.tsx +3 -3
  75. package/src/ui/editor/content-payload.ts +39 -0
  76. package/src/ui/inspector/FoundryTab.tsx +1 -1
  77. package/src/ui/pages/admin.astro +1 -1
  78. package/src/ui/screens/AuthorsScreen.tsx +1 -1
  79. package/src/ui/screens/ContentInsightsScreen.tsx +1 -1
  80. package/src/ui/screens/ContentRoute.tsx +4 -0
  81. package/src/ui/screens/EditorScreen.tsx +21 -9
  82. package/src/ui/screens/LibraryScreen.tsx +5 -2
  83. package/src/ui/screens/SettingsScreen.tsx +1 -1
  84. package/src/ui/screens/SubscriptionsScreen.tsx +2 -2
  85. package/src/ui/screens/registry.tsx +4 -0
  86. package/src/ui/styles/broadsheet.css +131 -156
  87. package/src/ui/styles/growth-labs-tokens.css +149 -0
  88. package/src/ui/theme.ts +29 -68
  89. package/src/ui/use-tweaks.ts +2 -1
@@ -18,8 +18,10 @@
18
18
  //
19
19
  // Compile-gated: all Tiptap/React usage verified by `pnpm run build` (tsc).
20
20
 
21
- import { useCallback, useEffect, useId, useRef, useState } from 'react'
21
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
22
+ import { type ContentMetadata, parseContentMetadata } from '../../engine/content-metadata.js'
22
23
  import { countWords, estimateReadTime } from '../../engine/publisher.js'
24
+ import type { ContentMetadataSelectField } from '../../integration/options.js'
23
25
  import type { ContentStatus, ContentType } from '../../schema/types.js'
24
26
  import { Icon } from '../icons.js'
25
27
  import { uploadMediaAsset } from '../screens/media-upload.js'
@@ -31,8 +33,10 @@ import {
31
33
  type ContentDraftFields,
32
34
  canCreateContentDraft,
33
35
  foundryDispatchSourceForDraft,
36
+ initialContentMetadata,
34
37
  normalizeMediaDurationSeconds,
35
38
  slugifyTitle,
39
+ updateContentMetadataSelection,
36
40
  } from './content-payload.js'
37
41
  import { measureEditorMediaDurationSeconds } from './editor-media-duration.js'
38
42
  import { uploadEditorImage } from './editor-media-upload.js'
@@ -63,6 +67,7 @@ export interface ContentFormProps {
63
67
  /** Called when save state changes so editor chrome can show a real save action. */
64
68
  onSaveStateChange?: (state: ContentSaveState) => void
65
69
  primaryCategories?: Array<{ slug: string; label: string }>
70
+ contentMetadataSelects?: ContentMetadataSelectField[]
66
71
  }
67
72
 
68
73
  export interface ContentSaveState {
@@ -106,6 +111,7 @@ interface ExistingContentItem {
106
111
  published_at?: number | null
107
112
  hero_image_id?: string | null
108
113
  hero_image_url?: string | null
114
+ metadata_json?: string
109
115
  }
110
116
 
111
117
  interface ExistingContentPayload {
@@ -149,6 +155,7 @@ interface FormState {
149
155
  publishedAt: string | null
150
156
  premium: boolean
151
157
  channel: string | null
158
+ metadata: ContentMetadata
152
159
  heroImageId: string
153
160
  heroImageUrl: string | null
154
161
  heroUploadStatus: 'idle' | 'uploading' | 'staged'
@@ -180,7 +187,9 @@ interface FormState {
180
187
  aiOpen: boolean
181
188
  }
182
189
 
183
- function createEmptyFormState(docId: string | null): FormState {
190
+ const EMPTY_CONTENT_METADATA_SELECTS: ContentMetadataSelectField[] = []
191
+
192
+ function createEmptyFormState(docId: string | null, metadata: ContentMetadata = {}): FormState {
184
193
  return {
185
194
  status: 'draft',
186
195
  title: '',
@@ -197,6 +206,7 @@ function createEmptyFormState(docId: string | null): FormState {
197
206
  publishedAt: null,
198
207
  premium: false,
199
208
  channel: null,
209
+ metadata,
200
210
  heroImageId: '',
201
211
  heroImageUrl: null,
202
212
  heroUploadStatus: 'idle',
@@ -238,8 +248,15 @@ export function ContentForm({
238
248
  onStatusChange,
239
249
  onSaveStateChange,
240
250
  primaryCategories = [],
251
+ contentMetadataSelects = EMPTY_CONTENT_METADATA_SELECTS,
241
252
  }: ContentFormProps) {
242
- const [form, setForm] = useState<FormState>(() => createEmptyFormState(docId))
253
+ const metadataFields = useMemo(
254
+ () => contentMetadataSelects.filter((field) => field.contentTypes.includes(contentType)),
255
+ [contentMetadataSelects, contentType],
256
+ )
257
+ const [form, setForm] = useState<FormState>(() =>
258
+ createEmptyFormState(docId, initialContentMetadata(metadataFields, contentType)),
259
+ )
243
260
 
244
261
  // Autosave state (mutable refs, not React state — no re-render needed)
245
262
  const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -260,7 +277,7 @@ export function ContentForm({
260
277
 
261
278
  const loadExistingContent = useCallback(async () => {
262
279
  if (!docId) {
263
- const next = createEmptyFormState(null)
280
+ const next = createEmptyFormState(null, initialContentMetadata(metadataFields, contentType))
264
281
  setForm(next)
265
282
  publishDraft(next)
266
283
  return
@@ -288,7 +305,7 @@ export function ContentForm({
288
305
  saveError: err instanceof Error ? err.message : 'Could not load content',
289
306
  }))
290
307
  }
291
- }, [contentType, docId, onStatusChange, publishDraft])
308
+ }, [contentType, docId, metadataFields, onStatusChange, publishDraft])
292
309
 
293
310
  // -- Word-count (derived from the body markdown) -------------------------
294
311
  const wordCount = countWords(form.body)
@@ -521,6 +538,16 @@ export function ContentForm({
521
538
  scheduleAutosave(next)
522
539
  }
523
540
 
541
+ function handleMetadataSelectChange(field: ContentMetadataSelectField, value: string) {
542
+ const next = {
543
+ ...latestForm.current,
544
+ metadata: updateContentMetadataSelection(latestForm.current.metadata, field, value),
545
+ }
546
+ setForm(next)
547
+ publishDraft(next)
548
+ scheduleAutosave(next)
549
+ }
550
+
524
551
  function handleBodyChange(md: string, portableText: string) {
525
552
  const next = { ...latestForm.current, body: md, bodyPortableText: portableText }
526
553
  setForm(next)
@@ -807,7 +834,7 @@ export function ContentForm({
807
834
  }
808
835
 
809
836
  return (
810
- <div style={formWrap}>
837
+ <div className="editor-document" style={formWrap}>
811
838
  {/* Autosave indicator + AI assist toggle */}
812
839
  <div style={saveIndicator}>
813
840
  <span
@@ -895,6 +922,39 @@ export function ContentForm({
895
922
  </div>
896
923
  )}
897
924
 
925
+ {metadataFields.map((field) => {
926
+ const storedValue = form.metadata[field.key]
927
+ const value = typeof storedValue === 'string' ? storedValue : ''
928
+ const inferred =
929
+ field.inferenceMarkerKey !== undefined && form.metadata[field.inferenceMarkerKey] === true
930
+ return (
931
+ <div key={field.key} style={fieldGroup}>
932
+ <label style={{ display: 'grid', gap: 6, fontSize: 12, color: 'var(--ink-muted)' }}>
933
+ {field.label}
934
+ <select
935
+ value={value}
936
+ onChange={(event) => handleMetadataSelectChange(field, event.currentTarget.value)}
937
+ aria-label={field.label}
938
+ style={urlInput}
939
+ >
940
+ <option value="">Select…</option>
941
+ {field.options.map((option) => (
942
+ <option key={option.value} value={option.value}>
943
+ {option.label}
944
+ </option>
945
+ ))}
946
+ </select>
947
+ </label>
948
+ {(field.helpText || inferred) && (
949
+ <div style={assetHelp}>
950
+ {inferred ? 'Inferred value — review and choose the best fit. ' : ''}
951
+ {field.helpText}
952
+ </div>
953
+ )}
954
+ </div>
955
+ )
956
+ })}
957
+
898
958
  {/* Signature masthead hairline between the standfirst and the body */}
899
959
  <hr className="masthead-rule" style={{ margin: '6px 0 20px' }} />
900
960
 
@@ -1457,6 +1517,7 @@ function contentResponseToFormState(
1457
1517
  next.publishedAt = unixSecondsToIso(item.published_at)
1458
1518
  next.premium = item.visibility === 'premium'
1459
1519
  next.channel = toNullableString(item.channel)
1520
+ next.metadata = storedContentMetadata(item.metadata_json)
1460
1521
  next.heroImageId = heroImageId
1461
1522
  next.heroImageUrl = heroImageUrl || (heroImageId ? mediaUrlFromId(heroImageId) : null)
1462
1523
 
@@ -1491,6 +1552,14 @@ function contentResponseToFormState(
1491
1552
  return next
1492
1553
  }
1493
1554
 
1555
+ function storedContentMetadata(raw: string | null | undefined): ContentMetadata {
1556
+ try {
1557
+ return parseContentMetadata(raw ?? '{}')
1558
+ } catch {
1559
+ return {}
1560
+ }
1561
+ }
1562
+
1494
1563
  function formToPreviewDraft(form: FormState, contentType: ContentType): ContentPreviewDraft {
1495
1564
  return {
1496
1565
  id: form.savedId ?? 'draft',
@@ -1567,7 +1636,6 @@ const formWrap: React.CSSProperties = {
1567
1636
  display: 'flex',
1568
1637
  flexDirection: 'column',
1569
1638
  gap: 0,
1570
- padding: '16px 24px',
1571
1639
  flex: 1,
1572
1640
  overflowY: 'auto',
1573
1641
  }
@@ -1589,11 +1657,11 @@ const titleInput: React.CSSProperties = {
1589
1657
  width: '100%',
1590
1658
  border: 0,
1591
1659
  background: 'transparent',
1592
- fontFamily: 'var(--serif)',
1660
+ fontFamily: 'var(--font-display)',
1593
1661
  fontWeight: 600,
1594
- fontSize: 36,
1595
- letterSpacing: '-0.025em',
1596
- lineHeight: 1.08,
1662
+ fontSize: 34,
1663
+ letterSpacing: '-0.02em',
1664
+ lineHeight: 1.15,
1597
1665
  color: 'var(--ink)',
1598
1666
  padding: 0,
1599
1667
  outline: 'none',
@@ -1603,8 +1671,8 @@ const dekInput: React.CSSProperties = {
1603
1671
  width: '100%',
1604
1672
  border: 0,
1605
1673
  background: 'transparent',
1606
- fontFamily: 'var(--serif)',
1607
- fontSize: 19,
1674
+ fontFamily: 'var(--font-body)',
1675
+ fontSize: 17,
1608
1676
  lineHeight: 1.5,
1609
1677
  color: 'var(--ink-soft)',
1610
1678
  padding: 0,
@@ -493,8 +493,8 @@ const contentWrap: React.CSSProperties = {
493
493
  flex: 1,
494
494
  padding: '20px 22px',
495
495
  overflowY: 'auto',
496
- fontFamily: 'var(--serif)',
497
- fontSize: 17,
498
- lineHeight: 1.75,
496
+ fontFamily: 'var(--font-body)',
497
+ fontSize: 15.5,
498
+ lineHeight: 1.65,
499
499
  color: 'var(--ink)',
500
500
  }
@@ -1,4 +1,6 @@
1
+ import type { ContentMetadata } from '../../engine/content-metadata.js'
1
2
  import { countWords, estimateReadTime } from '../../engine/publisher.js'
3
+ import type { ContentMetadataSelectField } from '../../integration/options.js'
2
4
  import type { ContentType } from '../../schema/types.js'
3
5
 
4
6
  export interface ContentDraftFields {
@@ -17,6 +19,42 @@ export interface ContentDraftFields {
17
19
  audioUrl: string
18
20
  durationSeconds: number | null
19
21
  channel?: string | null
22
+ metadata?: ContentMetadata
23
+ }
24
+
25
+ type MetadataSelectFieldContract = Pick<
26
+ ContentMetadataSelectField,
27
+ 'key' | 'defaultValue' | 'inferenceMarkerKey'
28
+ > & {
29
+ contentTypes: readonly ContentType[]
30
+ options: ReadonlyArray<{ value: string }>
31
+ }
32
+
33
+ export function initialContentMetadata(
34
+ fields: readonly MetadataSelectFieldContract[],
35
+ contentType: ContentType,
36
+ ): ContentMetadata {
37
+ const metadata: ContentMetadata = {}
38
+ for (const field of fields) {
39
+ if (!field.contentTypes.includes(contentType) || !field.defaultValue) continue
40
+ metadata[field.key] = field.defaultValue
41
+ }
42
+ return metadata
43
+ }
44
+
45
+ export function updateContentMetadataSelection(
46
+ metadata: Readonly<ContentMetadata>,
47
+ field: MetadataSelectFieldContract,
48
+ value: string,
49
+ ): ContentMetadata {
50
+ if (value && !field.options.some((option) => option.value === value)) {
51
+ throw new RangeError(`Unknown option for metadata field ${field.key}`)
52
+ }
53
+ const next: ContentMetadata = { ...metadata }
54
+ if (value) next[field.key] = value
55
+ else delete next[field.key]
56
+ if (field.inferenceMarkerKey) delete next[field.inferenceMarkerKey]
57
+ return next
20
58
  }
21
59
 
22
60
  export function normalizeMediaDurationSeconds(value: unknown): number | null {
@@ -97,6 +135,7 @@ export function buildContentUpdatePayload(
97
135
  title: draft.title,
98
136
  excerpt: draft.dek || null,
99
137
  }
138
+ if (draft.metadata !== undefined) payload.metadata = draft.metadata
100
139
  const channel = draft.channel?.trim()
101
140
  payload.primaryCategory = channel || null
102
141
  const heroImageId = draft.heroImageId?.trim()
@@ -499,7 +499,7 @@ const progressTrack: React.CSSProperties = {
499
499
  const progressFill: React.CSSProperties = {
500
500
  width: '100%',
501
501
  height: '100%',
502
- background: 'var(--gold)',
502
+ background: 'var(--accent-primary)',
503
503
  }
504
504
 
505
505
  const errorText: React.CSSProperties = {
@@ -22,7 +22,7 @@ const initial = { workspaces: config.workspaces, activeWorkspaceId: config.activ
22
22
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
23
23
  <link
24
24
  rel="stylesheet"
25
- href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;0,8..60,700;1,8..60,400&family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=DM+Mono:ital,wght@0,400;0,500;1,400&display=swap"
25
+ href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&family=Space+Grotesk:wght@400;500;600;700&display=swap"
26
26
  />
27
27
  </head>
28
28
  <body>
@@ -520,7 +520,7 @@ function initialsFor(name: string): string {
520
520
  return (name.trim().slice(0, 2) || '—').toUpperCase()
521
521
  }
522
522
 
523
- const AVATAR_COLORS = ['#0c4a6e', '#1f6b4a', '#5b4bc4', '#8c6d14', '#075985', '#475569']
523
+ const AVATAR_COLORS = ['#2F6FBD', '#1FA3A8', '#8075ED', '#F7A34B', '#2CA8E0', '#4D4D4D']
524
524
 
525
525
  function avatarColor(seed: string): string {
526
526
  let h = 0
@@ -171,7 +171,7 @@ export function ContentInsightsScreen() {
171
171
  style={{
172
172
  overflow: 'hidden',
173
173
  marginBottom: 'var(--gap)',
174
- borderLeft: `3px solid ${group.lane == null ? 'var(--hairline)' : 'var(--gold)'}`,
174
+ borderLeft: `3px solid ${group.lane == null ? 'var(--hairline)' : 'var(--accent-primary)'}`,
175
175
  }}
176
176
  >
177
177
  <header
@@ -8,6 +8,7 @@
8
8
  // This file creates thin stubs so the registry can compile now (Task 10).
9
9
 
10
10
  import { useReducer } from 'react'
11
+ import type { ContentMetadataSelectField } from '../../integration/options.js'
11
12
  import { type ContentType, contentViewReducer, initialContentView } from './content-view.js'
12
13
  import { EditorScreen } from './EditorScreen.js'
13
14
  import { LibraryScreen } from './LibraryScreen.js'
@@ -15,9 +16,11 @@ import { LibraryScreen } from './LibraryScreen.js'
15
16
  export function ContentRoute({
16
17
  allowInlineAuthorCreation = true,
17
18
  primaryCategories = [],
19
+ contentMetadataSelects = [],
18
20
  }: {
19
21
  allowInlineAuthorCreation?: boolean
20
22
  primaryCategories?: Array<{ slug: string; label: string }>
23
+ contentMetadataSelects?: ContentMetadataSelectField[]
21
24
  }) {
22
25
  const [view, dispatch] = useReducer(contentViewReducer, initialContentView)
23
26
 
@@ -49,6 +52,7 @@ export function ContentRoute({
49
52
  onClose={handleClose}
50
53
  allowInlineAuthorCreation={allowInlineAuthorCreation}
51
54
  primaryCategories={primaryCategories}
55
+ contentMetadataSelects={contentMetadataSelects}
52
56
  />
53
57
  )
54
58
  }
@@ -17,6 +17,7 @@
17
17
  // is Playwright-smoked only.
18
18
 
19
19
  import { useCallback, useState } from 'react'
20
+ import type { ContentMetadataSelectField } from '../../integration/options.js'
20
21
  import type { ContentStatus, ContentType } from '../../schema/types.js'
21
22
  import { buildSharePrefill, ShareModal } from '../components/ShareModal.js'
22
23
  import {
@@ -40,6 +41,7 @@ export interface EditorScreenProps {
40
41
  onClose: () => void
41
42
  allowInlineAuthorCreation?: boolean
42
43
  primaryCategories?: Array<{ slug: string; label: string }>
44
+ contentMetadataSelects?: ContentMetadataSelectField[]
43
45
  }
44
46
 
45
47
  // ---------------------------------------------------------------------------
@@ -58,12 +60,20 @@ const CONTENT_TYPE_LABELS: Record<ContentType, string> = {
58
60
 
59
61
  const STATUS_PILL_COLORS: Record<ContentStatus, string> = {
60
62
  draft: 'var(--ink-faint)',
61
- review: '#a78bfa',
62
- scheduled: '#60a5fa',
63
- published: '#34d399',
63
+ review: 'var(--amber)',
64
+ scheduled: 'var(--accent)',
65
+ published: 'var(--green)',
64
66
  archived: 'var(--ink-faint)',
65
67
  }
66
68
 
69
+ export function editorStatusColor(status: ContentStatus): string {
70
+ return STATUS_PILL_COLORS[status]
71
+ }
72
+
73
+ export function editorStatusBackground(status: ContentStatus): string {
74
+ return `color-mix(in srgb, ${editorStatusColor(status)} 14%, transparent)`
75
+ }
76
+
67
77
  // ---------------------------------------------------------------------------
68
78
  // EditorScreen
69
79
  // ---------------------------------------------------------------------------
@@ -73,6 +83,7 @@ export function EditorScreen({
73
83
  onClose,
74
84
  allowInlineAuthorCreation = true,
75
85
  primaryCategories = [],
86
+ contentMetadataSelects = [],
76
87
  }: EditorScreenProps) {
77
88
  const contentType: ContentType = (docRef.contentType as ContentType | undefined) ?? 'article'
78
89
  const siteBase = typeof window !== 'undefined' ? window.location.origin : undefined
@@ -369,6 +380,7 @@ export function EditorScreen({
369
380
  onStatusChange={setStatus}
370
381
  onSaveStateChange={handleSaveStateChange}
371
382
  primaryCategories={primaryCategories}
383
+ contentMetadataSelects={contentMetadataSelects}
372
384
  />
373
385
  </div>
374
386
 
@@ -503,8 +515,8 @@ function statusPill(status: ContentStatus): React.CSSProperties {
503
515
  fontWeight: 600,
504
516
  letterSpacing: '0.06em',
505
517
  textTransform: 'uppercase',
506
- background: `${STATUS_PILL_COLORS[status]}22`,
507
- color: STATUS_PILL_COLORS[status],
518
+ background: editorStatusBackground(status),
519
+ color: editorStatusColor(status),
508
520
  flexShrink: 0,
509
521
  }
510
522
  }
@@ -526,10 +538,10 @@ const headerBar: React.CSSProperties = {
526
538
  display: 'flex',
527
539
  alignItems: 'center',
528
540
  justifyContent: 'space-between',
529
- height: 48,
530
- padding: '0 16px',
541
+ height: 58,
542
+ padding: '0 20px',
531
543
  borderBottom: '1px solid var(--border)',
532
- background: 'var(--panel)',
544
+ background: 'var(--surface-raised-nav)',
533
545
  flexShrink: 0,
534
546
  gap: 12,
535
547
  }
@@ -661,7 +673,7 @@ const formArea: React.CSSProperties = {
661
673
  }
662
674
 
663
675
  const inspectorSlot: React.CSSProperties = {
664
- flex: '0 0 340px',
676
+ flex: '0 0 296px',
665
677
  borderLeft: '1px solid var(--border)',
666
678
  display: 'flex',
667
679
  flexDirection: 'column',
@@ -753,7 +753,10 @@ function ListRow({
753
753
  >
754
754
  {row.title || '(Untitled)'}
755
755
  {row.featured && (
756
- <span title="Premium" style={{ color: 'var(--gold)', flex: 'none' }}>
756
+ <span
757
+ title="Premium"
758
+ style={{ color: 'var(--accent-primary-strong)', flex: 'none' }}
759
+ >
757
760
  <Icon name="star" size={13} />
758
761
  </span>
759
762
  )}
@@ -1064,7 +1067,7 @@ function BoardCard({
1064
1067
  {row.updatedAt ? formatDate(row.updatedAt) : (row.authorName ?? '—')}
1065
1068
  </span>
1066
1069
  {row.featured && (
1067
- <span style={{ marginLeft: 'auto', color: 'var(--gold)' }}>
1070
+ <span style={{ marginLeft: 'auto', color: 'var(--accent-primary-strong)' }}>
1068
1071
  <Icon name="star" size={12} />
1069
1072
  </span>
1070
1073
  )}
@@ -375,7 +375,7 @@ function GeneralSettings({
375
375
  // Branding settings
376
376
  // ---------------------------------------------------------------------------
377
377
 
378
- const ACCENT_PRESETS = ['#0c4a6e', '#b23a2e', '#1f6b4a', '#5b4bc4', '#374151']
378
+ const ACCENT_PRESETS = ['#2F6FBD', '#1FA3A8', '#2CA8E0', '#8075ED', '#F56A61']
379
379
  const TYPEFACE_OPTIONS = ['serif', 'Source Serif 4', 'Spectral', 'Newsreader', 'Inter']
380
380
 
381
381
  function BrandingSettings({
@@ -1310,7 +1310,7 @@ function KpiCard({
1310
1310
  // PlanMixChart — stacked bar + legend rows (aside panel)
1311
1311
  // ---------------------------------------------------------------------------
1312
1312
 
1313
- const PLAN_DOT_COLORS = ['#1f6b4a', '#0c4a6e', '#5b4bc4', '#8c6d14', '#475569']
1313
+ const PLAN_DOT_COLORS = ['#1FA3A8', '#2F6FBD', '#8075ED', '#F7A34B', '#4D4D4D']
1314
1314
 
1315
1315
  function PlanMixChart({
1316
1316
  planMix,
@@ -1746,7 +1746,7 @@ function initialsFor(email: string): string {
1746
1746
  return local.slice(0, 2).toUpperCase()
1747
1747
  }
1748
1748
 
1749
- const AVATAR_COLORS = ['#0c4a6e', '#1f6b4a', '#5b4bc4', '#8c6d14', '#075985', '#475569']
1749
+ const AVATAR_COLORS = ['#2F6FBD', '#1FA3A8', '#8075ED', '#F7A34B', '#2CA8E0', '#4D4D4D']
1750
1750
 
1751
1751
  function avatarColor(seed: string): string {
1752
1752
  let h = 0
@@ -11,6 +11,7 @@
11
11
  // internal router state (which is not exported). The ContentRoute itself owns
12
12
  // the full open-doc reducer (which doc is open, new vs existing).
13
13
 
14
+ import type { ContentMetadataSelectField } from '../../integration/options.js'
14
15
  import type { ScreenRegistry } from '../components/CmsApp.js'
15
16
  import { useHashRouter } from '../use-hash-router.js'
16
17
  import { AnalyticsScreen } from './AnalyticsScreen.js'
@@ -42,10 +43,12 @@ function CalendarRoute() {
42
43
  export function createCmsScreens({
43
44
  allowInlineAuthorCreation = true,
44
45
  primaryCategories = [],
46
+ contentMetadataSelects = [],
45
47
  surveyId,
46
48
  }: {
47
49
  allowInlineAuthorCreation?: boolean
48
50
  primaryCategories?: Array<{ slug: string; label: string }>
51
+ contentMetadataSelects?: ContentMetadataSelectField[]
49
52
  surveyId?: string
50
53
  } = {}): ScreenRegistry {
51
54
  return {
@@ -53,6 +56,7 @@ export function createCmsScreens({
53
56
  <ContentRoute
54
57
  allowInlineAuthorCreation={allowInlineAuthorCreation}
55
58
  primaryCategories={primaryCategories}
59
+ contentMetadataSelects={contentMetadataSelects}
56
60
  />
57
61
  ),
58
62
  'content-insights': () => <ContentInsightsScreen />,