@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.
- package/README.md +20 -0
- package/dist/engine/slug.d.ts.map +1 -1
- package/dist/engine/slug.js +11 -1
- package/dist/engine/slug.js.map +1 -1
- package/dist/integration/options.d.ts +196 -0
- package/dist/integration/options.d.ts.map +1 -1
- package/dist/integration/options.js +62 -0
- package/dist/integration/options.js.map +1 -1
- package/dist/routes/content.d.ts.map +1 -1
- package/dist/routes/content.js +30 -0
- package/dist/routes/content.js.map +1 -1
- package/dist/ui/client/mount.d.ts +1 -0
- package/dist/ui/client/mount.d.ts.map +1 -1
- package/dist/ui/client/mount.js +2 -0
- package/dist/ui/client/mount.js.map +1 -1
- package/dist/ui/components/CmsApp.js +2 -11
- package/dist/ui/components/CmsApp.js.map +1 -1
- package/dist/ui/components/Sidebar.d.ts.map +1 -1
- package/dist/ui/components/Sidebar.js +2 -2
- package/dist/ui/components/Sidebar.js.map +1 -1
- package/dist/ui/components/Topbar.d.ts.map +1 -1
- package/dist/ui/components/Topbar.js +1 -10
- package/dist/ui/components/Topbar.js.map +1 -1
- package/dist/ui/editor/ContentForm.d.ts +3 -1
- package/dist/ui/editor/ContentForm.d.ts.map +1 -1
- package/dist/ui/editor/ContentForm.js +42 -16
- package/dist/ui/editor/ContentForm.js.map +1 -1
- package/dist/ui/editor/Rte.js +3 -3
- package/dist/ui/editor/Rte.js.map +1 -1
- package/dist/ui/editor/content-payload.d.ts +12 -0
- package/dist/ui/editor/content-payload.d.ts.map +1 -1
- package/dist/ui/editor/content-payload.js +24 -0
- package/dist/ui/editor/content-payload.js.map +1 -1
- package/dist/ui/inspector/FoundryTab.js +1 -1
- package/dist/ui/inspector/FoundryTab.js.map +1 -1
- package/dist/ui/pages/admin.astro +1 -1
- package/dist/ui/screens/AuthorsScreen.js +1 -1
- package/dist/ui/screens/ContentInsightsScreen.js +1 -1
- package/dist/ui/screens/ContentInsightsScreen.js.map +1 -1
- package/dist/ui/screens/ContentRoute.d.ts +3 -1
- package/dist/ui/screens/ContentRoute.d.ts.map +1 -1
- package/dist/ui/screens/ContentRoute.js +2 -2
- package/dist/ui/screens/ContentRoute.js.map +1 -1
- package/dist/ui/screens/EditorScreen.d.ts +6 -1
- package/dist/ui/screens/EditorScreen.d.ts.map +1 -1
- package/dist/ui/screens/EditorScreen.js +17 -11
- package/dist/ui/screens/EditorScreen.js.map +1 -1
- package/dist/ui/screens/LibraryScreen.js +2 -2
- package/dist/ui/screens/LibraryScreen.js.map +1 -1
- package/dist/ui/screens/SettingsScreen.js +1 -1
- package/dist/ui/screens/SubscriptionsScreen.js +2 -2
- package/dist/ui/screens/registry.d.ts +3 -1
- package/dist/ui/screens/registry.d.ts.map +1 -1
- package/dist/ui/screens/registry.js +2 -2
- package/dist/ui/screens/registry.js.map +1 -1
- package/dist/ui/styles/broadsheet.css +131 -156
- package/dist/ui/styles/growth-labs-tokens.css +149 -0
- package/dist/ui/theme.d.ts +7 -4
- package/dist/ui/theme.d.ts.map +1 -1
- package/dist/ui/theme.js +24 -65
- package/dist/ui/theme.js.map +1 -1
- package/dist/ui/use-tweaks.d.ts.map +1 -1
- package/dist/ui/use-tweaks.js +2 -1
- package/dist/ui/use-tweaks.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/slug.ts +12 -1
- package/src/integration/options.ts +69 -0
- package/src/routes/content.ts +37 -0
- package/src/ui/client/mount.tsx +2 -0
- package/src/ui/components/CmsApp.tsx +3 -20
- package/src/ui/components/Sidebar.tsx +5 -11
- package/src/ui/components/Topbar.tsx +2 -13
- package/src/ui/editor/ContentForm.tsx +81 -13
- package/src/ui/editor/Rte.tsx +3 -3
- package/src/ui/editor/content-payload.ts +39 -0
- package/src/ui/inspector/FoundryTab.tsx +1 -1
- package/src/ui/pages/admin.astro +1 -1
- package/src/ui/screens/AuthorsScreen.tsx +1 -1
- package/src/ui/screens/ContentInsightsScreen.tsx +1 -1
- package/src/ui/screens/ContentRoute.tsx +4 -0
- package/src/ui/screens/EditorScreen.tsx +21 -9
- package/src/ui/screens/LibraryScreen.tsx +5 -2
- package/src/ui/screens/SettingsScreen.tsx +1 -1
- package/src/ui/screens/SubscriptionsScreen.tsx +2 -2
- package/src/ui/screens/registry.tsx +4 -0
- package/src/ui/styles/broadsheet.css +131 -156
- package/src/ui/styles/growth-labs-tokens.css +149 -0
- package/src/ui/theme.ts +29 -68
- package/src/ui/use-tweaks.ts +2 -1
package/dist/ui/theme.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/ui/theme.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/ui/theme.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,SAAS;IACzB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACZ;AACD,KAAK,eAAe,GAAG,aAAa,CAAA;AACpC,MAAM,MAAM,UAAU,GAAG,eAAe,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAEzF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,eAAe,EAAE,SAAS,CAQtD,CAAA;AACD,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,eAAe,EAAE,SAAS,CAQ3D,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;AACxC,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;AACrD,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAA;AAEnD,eAAO,MAAM,cAAc;WACR,SAAS;YACF,UAAU;aACb,OAAO;gBACR,UAAU;;CAE/B,CAAA;AAID,wBAAgB,QAAQ,CAAC,aAAa,EAAE,OAAO,GAAG,MAAM,CAEvD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAEvD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAU9F"}
|
package/dist/ui/theme.js
CHANGED
|
@@ -1,81 +1,40 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// the useTweaks effect; broadsheet.css carries the rest.
|
|
1
|
+
// Compatibility helpers for the fixed Growth Labs visual contract. Theme and
|
|
2
|
+
// density remain user preferences; brand accent and typography do not.
|
|
4
3
|
export const ACCENTS = {
|
|
5
|
-
'
|
|
6
|
-
a: '#
|
|
7
|
-
s: '#
|
|
8
|
-
fg: '#
|
|
9
|
-
tint: 'rgba(
|
|
10
|
-
ring: '
|
|
11
|
-
},
|
|
12
|
-
'Press red': {
|
|
13
|
-
a: '#b23a2e',
|
|
14
|
-
s: '#962f25',
|
|
15
|
-
fg: '#ffffff',
|
|
16
|
-
tint: 'rgba(178,58,46,0.10)',
|
|
17
|
-
ring: 'rgba(178,58,46,0.38)',
|
|
18
|
-
},
|
|
19
|
-
Forest: {
|
|
20
|
-
a: '#1f6b4a',
|
|
21
|
-
s: '#19583d',
|
|
22
|
-
fg: '#ffffff',
|
|
23
|
-
tint: 'rgba(31,107,74,0.10)',
|
|
24
|
-
ring: 'rgba(31,107,74,0.38)',
|
|
25
|
-
},
|
|
26
|
-
Violet: {
|
|
27
|
-
a: '#5b4bc4',
|
|
28
|
-
s: '#4c3ea8',
|
|
29
|
-
fg: '#ffffff',
|
|
30
|
-
tint: 'rgba(91,75,196,0.10)',
|
|
31
|
-
ring: 'rgba(91,75,196,0.38)',
|
|
4
|
+
'Growth Labs': {
|
|
5
|
+
a: '#2F6FBD',
|
|
6
|
+
s: '#245CA8',
|
|
7
|
+
fg: '#FFFFFF',
|
|
8
|
+
tint: 'rgba(62,209,198,0.14)',
|
|
9
|
+
ring: '#1E5EA8',
|
|
32
10
|
},
|
|
33
11
|
};
|
|
34
12
|
export const ACCENTS_DARK = {
|
|
35
|
-
'
|
|
36
|
-
a: '#
|
|
37
|
-
s: '#
|
|
38
|
-
fg: '#
|
|
39
|
-
tint: 'rgba(
|
|
40
|
-
ring: '
|
|
41
|
-
},
|
|
42
|
-
'Press red': {
|
|
43
|
-
a: '#ef6e62',
|
|
44
|
-
s: '#e05348',
|
|
45
|
-
fg: '#1d0a08',
|
|
46
|
-
tint: 'rgba(239,110,98,0.16)',
|
|
47
|
-
ring: 'rgba(239,110,98,0.45)',
|
|
48
|
-
},
|
|
49
|
-
Forest: {
|
|
50
|
-
a: '#4ec98a',
|
|
51
|
-
s: '#3bb476',
|
|
52
|
-
fg: '#06160f',
|
|
53
|
-
tint: 'rgba(78,201,138,0.16)',
|
|
54
|
-
ring: 'rgba(78,201,138,0.45)',
|
|
55
|
-
},
|
|
56
|
-
Violet: {
|
|
57
|
-
a: '#9d8df0',
|
|
58
|
-
s: '#8978e6',
|
|
59
|
-
fg: '#0e0a1d',
|
|
60
|
-
tint: 'rgba(157,141,240,0.16)',
|
|
61
|
-
ring: 'rgba(157,141,240,0.45)',
|
|
13
|
+
'Growth Labs': {
|
|
14
|
+
a: '#3F86D8',
|
|
15
|
+
s: '#2F6FBD',
|
|
16
|
+
fg: '#FFFFFF',
|
|
17
|
+
tint: 'rgba(62,209,198,0.18)',
|
|
18
|
+
ring: '#50F5EC',
|
|
62
19
|
},
|
|
63
20
|
};
|
|
64
21
|
export const TWEAK_DEFAULTS = {
|
|
65
|
-
theme: '
|
|
66
|
-
accent: '
|
|
22
|
+
theme: 'light',
|
|
23
|
+
accent: 'Growth Labs',
|
|
67
24
|
density: 'regular',
|
|
68
25
|
previewPos: 'off',
|
|
69
|
-
serifDisplay:
|
|
26
|
+
serifDisplay: false,
|
|
70
27
|
};
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
28
|
+
const DISPLAY = '"Space Grotesk", "Lato", system-ui, sans-serif';
|
|
29
|
+
export function serifVar(_serifDisplay) {
|
|
30
|
+
return DISPLAY;
|
|
31
|
+
}
|
|
32
|
+
export function inverseTextVar(theme) {
|
|
33
|
+
return theme === 'dark' ? '#2B2F33' : '#FFFFFF';
|
|
75
34
|
}
|
|
76
35
|
export function resolveAccentVars(theme, accent) {
|
|
77
36
|
const table = theme === 'dark' ? ACCENTS_DARK : ACCENTS;
|
|
78
|
-
const set = table[accent] ?? table['
|
|
37
|
+
const set = table[accent] ?? table['Growth Labs'];
|
|
79
38
|
return {
|
|
80
39
|
'--accent': set.a,
|
|
81
40
|
'--accent-strong': set.s,
|
package/dist/ui/theme.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/ui/theme.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/ui/theme.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,uEAAuE;AAYvE,MAAM,CAAC,MAAM,OAAO,GAAuC;IAC1D,aAAa,EAAE;QACd,CAAC,EAAE,SAAS;QACZ,CAAC,EAAE,SAAS;QACZ,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,uBAAuB;QAC7B,IAAI,EAAE,SAAS;KACf;CACD,CAAA;AACD,MAAM,CAAC,MAAM,YAAY,GAAuC;IAC/D,aAAa,EAAE;QACd,CAAC,EAAE,SAAS;QACZ,CAAC,EAAE,SAAS;QACZ,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,uBAAuB;QAC7B,IAAI,EAAE,SAAS;KACf;CACD,CAAA;AAMD,MAAM,CAAC,MAAM,cAAc,GAAG;IAC7B,KAAK,EAAE,OAAoB;IAC3B,MAAM,EAAE,aAA2B;IACnC,OAAO,EAAE,SAAoB;IAC7B,UAAU,EAAE,KAAmB;IAC/B,YAAY,EAAE,KAAK;CACnB,CAAA;AAED,MAAM,OAAO,GAAG,gDAAgD,CAAA;AAEhE,MAAM,UAAU,QAAQ,CAAC,aAAsB;IAC9C,OAAO,OAAO,CAAA;AACf,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAgB;IAC9C,OAAO,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;AAChD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAgB,EAAE,MAAkB;IACrE,MAAM,KAAK,GAAG,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAA;IACvD,MAAM,GAAG,GAAG,KAAK,CAAC,MAAyB,CAAC,IAAI,KAAK,CAAC,aAAa,CAAC,CAAA;IACpE,OAAO;QACN,UAAU,EAAE,GAAG,CAAC,CAAC;QACjB,iBAAiB,EAAE,GAAG,CAAC,CAAC;QACxB,aAAa,EAAE,GAAG,CAAC,EAAE;QACrB,eAAe,EAAE,GAAG,CAAC,IAAI;QACzB,eAAe,EAAE,GAAG,CAAC,IAAI;KACzB,CAAA;AACF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-tweaks.d.ts","sourceRoot":"","sources":["../../src/ui/use-tweaks.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAEjF,wBAAgB,SAAS,IAAI;IAC5B,CAAC,EAAE,UAAU,CAAA;IACb,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,MAAM,UAAU,CAAC,KAAK,IAAI,CAAA;CAC/F,
|
|
1
|
+
{"version":3,"file":"use-tweaks.d.ts","sourceRoot":"","sources":["../../src/ui/use-tweaks.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAEjF,wBAAgB,SAAS,IAAI;IAC5B,CAAC,EAAE,UAAU,CAAA;IACb,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,MAAM,UAAU,CAAC,KAAK,IAAI,CAAA;CAC/F,CAwBA"}
|
package/dist/ui/use-tweaks.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
-
import { resolveAccentVars, serifVar } from './theme.js';
|
|
2
|
+
import { inverseTextVar, resolveAccentVars, serifVar } from './theme.js';
|
|
3
3
|
import { applyTweak, loadTweaks, saveTweaks } from './tweaks.js';
|
|
4
4
|
export function useTweaks() {
|
|
5
5
|
const [t, setState] = useState(() => typeof localStorage !== 'undefined' ? loadTweaks(localStorage) : {});
|
|
@@ -18,6 +18,7 @@ export function useTweaks() {
|
|
|
18
18
|
for (const [k, val] of Object.entries(resolveAccentVars(t.theme, t.accent)))
|
|
19
19
|
root.style.setProperty(k, val);
|
|
20
20
|
root.style.setProperty('--serif', serifVar(t.serifDisplay));
|
|
21
|
+
root.style.setProperty('--text-on-inverse', inverseTextVar(t.theme));
|
|
21
22
|
}, [t.theme, t.density, t.accent, t.serifDisplay]);
|
|
22
23
|
return { t, setTweak };
|
|
23
24
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-tweaks.js","sourceRoot":"","sources":["../../src/ui/use-tweaks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"use-tweaks.js","sourceRoot":"","sources":["../../src/ui/use-tweaks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACxD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACxE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAmB,MAAM,aAAa,CAAA;AAEjF,MAAM,UAAU,SAAS;IAIxB,MAAM,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAa,GAAG,EAAE,CAC/C,OAAO,YAAY,KAAK,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,CAAE,EAAiB,CACnF,CAAA;IACD,MAAM,QAAQ,GAAG,WAAW,CAC3B,CAAC,CAAyC,EAAE,CAAgC,EAAE,EAAE;QAC/E,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE;YACjB,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;YACnC,IAAI,OAAO,YAAY,KAAK,WAAW;gBAAE,UAAU,CAAC,YAAY,EAAE,IAAI,CAAC,CAAA;YACvE,OAAO,IAAI,CAAA;QACZ,CAAC,CAAC,CAAA;IACH,CAAC,EACD,EAAE,CACF,CAAA;IACD,SAAS,CAAC,GAAG,EAAE;QACd,MAAM,IAAI,GAAG,QAAQ,CAAC,eAAe,CAAA;QACrC,IAAI,CAAC,YAAY,CAAC,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAA;QACxC,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;QAC5C,KAAK,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;YAC1E,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;QAC/B,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAA;QAC3D,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,mBAAmB,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;IACrE,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,CAAA;IAClD,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAA;AACvB,CAAC"}
|
package/package.json
CHANGED
package/src/engine/slug.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
// Slug helpers ported verbatim from fulcrum-labs/fronts src/lib/slug.ts
|
|
2
2
|
// (inventory §1: `slug.ts` — extract, pure). No Fronts coupling.
|
|
3
3
|
|
|
4
|
+
// Straight quotes/backtick plus every smart-quote and prime variant editors'
|
|
5
|
+
// paste sources (Word, Google Docs, iOS) actually produce. Stripping only the
|
|
6
|
+
// straight forms let curly apostrophes survive as literal characters instead
|
|
7
|
+
// of being removed before the non-alphanumeric collapse below, so
|
|
8
|
+
// "Ukraine's" slugified to "ukraine-s" instead of "ukraines" (packages#397).
|
|
9
|
+
// Covers, via \uXXXX escapes (not literal glyphs, for unambiguous
|
|
10
|
+
// editors/diffs): ' " ` (straight) ‘-‟ (curly single/double quote
|
|
11
|
+
// family, incl. low-9/reversed-9 variants) ′/″ (prime/double
|
|
12
|
+
// prime) ʼ (modifier letter apostrophe).
|
|
13
|
+
const QUOTE_CHARS = /['"`‘-‟′″ʼ]/g
|
|
14
|
+
|
|
4
15
|
export function slugify(input: string): string {
|
|
5
16
|
return input
|
|
6
17
|
.toLowerCase()
|
|
7
|
-
.replace(
|
|
18
|
+
.replace(QUOTE_CHARS, '')
|
|
8
19
|
.replace(/[^a-z0-9]+/g, '-')
|
|
9
20
|
.replace(/^-+|-+$/g, '')
|
|
10
21
|
.replace(/--+/g, '-')
|
|
@@ -13,6 +13,74 @@ const primaryCategorySchema = z.object({
|
|
|
13
13
|
label: z.string().min(1),
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
+
const contentMetadataKeySchema = z
|
|
17
|
+
.string()
|
|
18
|
+
.regex(/^[a-z][a-z0-9_]{0,63}$/, 'metadata keys must be lowercase snake_case')
|
|
19
|
+
|
|
20
|
+
const contentMetadataSelectFieldSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
key: contentMetadataKeySchema,
|
|
23
|
+
label: z.string().trim().min(1).max(100),
|
|
24
|
+
helpText: z.string().trim().min(1).max(500).optional(),
|
|
25
|
+
contentTypes: z.array(z.enum(['article', 'video', 'podcast', 'newsletter', 'page'])).min(1),
|
|
26
|
+
options: z
|
|
27
|
+
.array(
|
|
28
|
+
z
|
|
29
|
+
.object({
|
|
30
|
+
value: z.string().trim().min(1).max(100),
|
|
31
|
+
label: z.string().trim().min(1).max(100),
|
|
32
|
+
})
|
|
33
|
+
.strict(),
|
|
34
|
+
)
|
|
35
|
+
.min(1),
|
|
36
|
+
defaultValue: z.string().trim().min(1).max(100).optional(),
|
|
37
|
+
inferenceMarkerKey: contentMetadataKeySchema.optional(),
|
|
38
|
+
})
|
|
39
|
+
.strict()
|
|
40
|
+
.superRefine((field, ctx) => {
|
|
41
|
+
const values = field.options.map(({ value }) => value)
|
|
42
|
+
if (new Set(values).size !== values.length) {
|
|
43
|
+
ctx.addIssue({
|
|
44
|
+
code: 'custom',
|
|
45
|
+
path: ['options'],
|
|
46
|
+
message: 'metadata select option values must be unique',
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
if (field.defaultValue && !values.includes(field.defaultValue)) {
|
|
50
|
+
ctx.addIssue({
|
|
51
|
+
code: 'custom',
|
|
52
|
+
path: ['defaultValue'],
|
|
53
|
+
message: 'metadata select defaultValue must match a declared option',
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
if (field.inferenceMarkerKey === field.key) {
|
|
57
|
+
ctx.addIssue({
|
|
58
|
+
code: 'custom',
|
|
59
|
+
path: ['inferenceMarkerKey'],
|
|
60
|
+
message: 'metadata select inference marker must use a separate key',
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const contentMetadataSelectsSchema = z
|
|
66
|
+
.array(contentMetadataSelectFieldSchema)
|
|
67
|
+
.superRefine((fields, ctx) => {
|
|
68
|
+
const keys = new Set<string>()
|
|
69
|
+
for (const [index, field] of fields.entries()) {
|
|
70
|
+
if (keys.has(field.key)) {
|
|
71
|
+
ctx.addIssue({
|
|
72
|
+
code: 'custom',
|
|
73
|
+
path: [index, 'key'],
|
|
74
|
+
message: 'metadata select keys must be unique',
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
keys.add(field.key)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
.default([])
|
|
81
|
+
|
|
82
|
+
export type ContentMetadataSelectField = z.output<typeof contentMetadataSelectFieldSchema>
|
|
83
|
+
|
|
16
84
|
const themeSchema = z
|
|
17
85
|
.object({
|
|
18
86
|
mode: z.enum(['light', 'dark']).default('dark'),
|
|
@@ -38,6 +106,7 @@ export const cmsIntegrationOptionsSchema = z
|
|
|
38
106
|
.object({
|
|
39
107
|
workspaces: z.array(workspaceSchema).min(1, 'at least one workspace is required'),
|
|
40
108
|
primaryCategories: z.array(primaryCategorySchema).default([]),
|
|
109
|
+
contentMetadataSelects: contentMetadataSelectsSchema,
|
|
41
110
|
activeWorkspaceId: z.string().optional(),
|
|
42
111
|
adminBasePath: z.string().default('/admin'),
|
|
43
112
|
searchEnabled: z.boolean().default(true),
|
package/src/routes/content.ts
CHANGED
|
@@ -18,6 +18,11 @@ import { z } from 'zod'
|
|
|
18
18
|
import { logActivity } from '../engine/activity-log.js'
|
|
19
19
|
import { generateContentDek } from '../engine/ai-dek.js'
|
|
20
20
|
import { createDekFailureNotifications } from '../engine/ai-dek-notifications.js'
|
|
21
|
+
import {
|
|
22
|
+
type ContentMetadata,
|
|
23
|
+
ContentMetadataError,
|
|
24
|
+
serializeContentMetadata,
|
|
25
|
+
} from '../engine/content-metadata.js'
|
|
21
26
|
import { listContributors, setContributors } from '../engine/contributors.js'
|
|
22
27
|
import type { D1Database } from '../engine/d1.js'
|
|
23
28
|
import {
|
|
@@ -52,6 +57,7 @@ import {
|
|
|
52
57
|
import { applySlugRenameRedirects } from '../engine/slug-redirects.js'
|
|
53
58
|
import { softDeleteContent } from '../engine/soft-delete.js'
|
|
54
59
|
import { TagInputError } from '../engine/taxonomy.js'
|
|
60
|
+
import type { ValidationError } from '../engine/validator/index.js'
|
|
55
61
|
import { parseLayoutEnvelope } from '../schema/layout.js'
|
|
56
62
|
import { parsePortableTextEnvelope } from '../schema/portable-text.js'
|
|
57
63
|
import type { ContentStatus } from '../schema/types.js'
|
|
@@ -284,6 +290,21 @@ const BodyContentCreateSchema = z.object({
|
|
|
284
290
|
editorTakeaways: z.array(z.string().min(1)).max(4).optional().nullable(),
|
|
285
291
|
})
|
|
286
292
|
|
|
293
|
+
const ContentMetadataSchema = z
|
|
294
|
+
.record(z.string(), z.unknown())
|
|
295
|
+
.superRefine((metadata, ctx) => {
|
|
296
|
+
try {
|
|
297
|
+
serializeContentMetadata(metadata as ContentMetadata)
|
|
298
|
+
} catch (error) {
|
|
299
|
+
ctx.addIssue({
|
|
300
|
+
code: 'custom',
|
|
301
|
+
message:
|
|
302
|
+
error instanceof ContentMetadataError ? error.message : 'content metadata is invalid',
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
.transform((metadata) => metadata as ContentMetadata)
|
|
307
|
+
|
|
287
308
|
function buildSchemas(resolved: ReturnType<typeof resolveConfig>) {
|
|
288
309
|
const category = slugEnum(resolved.primaryCategorySlugs)
|
|
289
310
|
const topic = slugEnum(resolved.primaryTopicSlugs)
|
|
@@ -306,6 +327,7 @@ function buildSchemas(resolved: ReturnType<typeof resolveConfig>) {
|
|
|
306
327
|
socialImageId: z.string().optional().nullable(),
|
|
307
328
|
canonicalUrl: z.string().url().optional().nullable(),
|
|
308
329
|
tags: z.array(z.string()).optional(),
|
|
330
|
+
metadata: ContentMetadataSchema.optional(),
|
|
309
331
|
})
|
|
310
332
|
|
|
311
333
|
const CreateSchema = z.discriminatedUnion('type', [
|
|
@@ -370,6 +392,7 @@ function buildSchemas(resolved: ReturnType<typeof resolveConfig>) {
|
|
|
370
392
|
socialImageId: z.string().optional().nullable(),
|
|
371
393
|
canonicalUrl: z.string().url().optional().nullable(),
|
|
372
394
|
tags: z.array(z.string()).optional(),
|
|
395
|
+
metadata: ContentMetadataSchema.optional(),
|
|
373
396
|
content: z.record(z.string(), z.any()).optional(),
|
|
374
397
|
relations: z.array(RelationSchema).optional(),
|
|
375
398
|
aiLockedFields: z.array(z.string()).optional(),
|
|
@@ -1205,6 +1228,12 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1205
1228
|
return json({ error: 'Invalid request', details: parsed.error.flatten() }, 400)
|
|
1206
1229
|
}
|
|
1207
1230
|
|
|
1231
|
+
// Warn-severity body-lint results. Declared here so the success payload
|
|
1232
|
+
// below can carry them: they do not block the save, but an editor who is
|
|
1233
|
+
// never told is how a malformed Sources section reached a published page
|
|
1234
|
+
// (packages#329).
|
|
1235
|
+
let bodyWarnings: ValidationError[] = []
|
|
1236
|
+
|
|
1208
1237
|
if (parsed.data.content !== undefined) {
|
|
1209
1238
|
const contentSchema = getContentUpdateSchema(existing.type)
|
|
1210
1239
|
if (!contentSchema) return json({ error: 'Invalid content type' }, 400)
|
|
@@ -1243,6 +1272,11 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1243
1272
|
422,
|
|
1244
1273
|
)
|
|
1245
1274
|
}
|
|
1275
|
+
// Warn-severity violations do not block the save, but the editor
|
|
1276
|
+
// has to be told: an unreported warning is why a malformed Sources
|
|
1277
|
+
// section reached a published page (packages#329). Carried into
|
|
1278
|
+
// the 200 payload below rather than only console.warn'd.
|
|
1279
|
+
bodyWarnings = guard.warnings
|
|
1246
1280
|
}
|
|
1247
1281
|
}
|
|
1248
1282
|
}
|
|
@@ -1276,6 +1310,9 @@ export function createContentRoutes(config: ContentRouteConfig): ContentRouteHan
|
|
|
1276
1310
|
podcastFoundryQueued: Boolean(podcastAttempt.result),
|
|
1277
1311
|
podcastFoundryCorrelationId: podcastAttempt.result?.correlationId ?? null,
|
|
1278
1312
|
podcastFoundryReason: podcastAttempt.reason,
|
|
1313
|
+
// Present and empty when the body linted clean, so a client can
|
|
1314
|
+
// distinguish "no warnings" from "this build does not report them".
|
|
1315
|
+
bodyWarnings,
|
|
1279
1316
|
})
|
|
1280
1317
|
},
|
|
1281
1318
|
|
package/src/ui/client/mount.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { CmsApp } from '../components/CmsApp.js'
|
|
|
5
5
|
import { createCmsScreens } from '../screens/registry.js'
|
|
6
6
|
import { initialWorkspaceState } from '../workspace-context.js'
|
|
7
7
|
import { type MeResponseSummary, resolveBootState } from './boot-state.js'
|
|
8
|
+
import '../styles/growth-labs-tokens.css'
|
|
8
9
|
import '../styles/broadsheet.css'
|
|
9
10
|
|
|
10
11
|
async function fetchMe(base: string): Promise<MeResponseSummary | null> {
|
|
@@ -38,6 +39,7 @@ export async function mount(elementId = 'masthead-root') {
|
|
|
38
39
|
const screens = createCmsScreens({
|
|
39
40
|
allowInlineAuthorCreation: config.socialSharing.allowInlineAuthorCreation,
|
|
40
41
|
primaryCategories: config.primaryCategories,
|
|
42
|
+
contentMetadataSelects: config.contentMetadataSelects,
|
|
41
43
|
surveyId: config.surveyResults?.surveyId,
|
|
42
44
|
})
|
|
43
45
|
createRoot(el).render(<CmsApp initial={initial} screens={screens} boot={boot} />)
|
|
@@ -99,11 +99,7 @@ export function resolveScreenForAccess(
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
function EmptyRoute({ route }: { route: Route }) {
|
|
102
|
-
return
|
|
103
|
-
<div className="page" style={{ padding: 40, color: 'var(--ink-muted)' }}>
|
|
104
|
-
The "{route}" screen mounts here.
|
|
105
|
-
</div>
|
|
106
|
-
)
|
|
102
|
+
return <div className="page shell-state">The "{route}" screen mounts here.</div>
|
|
107
103
|
}
|
|
108
104
|
|
|
109
105
|
// Render the active screen as a real child of the error boundary. The registry
|
|
@@ -135,26 +131,13 @@ class ScreenErrorBoundary extends Component<
|
|
|
135
131
|
render() {
|
|
136
132
|
if (this.state.error) {
|
|
137
133
|
return (
|
|
138
|
-
<div className="page"
|
|
134
|
+
<div className="page shell-state">
|
|
139
135
|
<h1 className="page-title">Something went wrong</h1>
|
|
140
136
|
<div className="page-sub" style={{ marginTop: 8 }}>
|
|
141
137
|
The “{this.props.route}” screen hit an error and couldn’t render. The rest of the CMS is
|
|
142
138
|
unaffected — use the sidebar to navigate elsewhere.
|
|
143
139
|
</div>
|
|
144
|
-
<pre
|
|
145
|
-
style={{
|
|
146
|
-
marginTop: 16,
|
|
147
|
-
padding: 12,
|
|
148
|
-
fontSize: 12,
|
|
149
|
-
color: 'var(--red)',
|
|
150
|
-
whiteSpace: 'pre-wrap',
|
|
151
|
-
wordBreak: 'break-word',
|
|
152
|
-
background: 'var(--surface-sunken, rgba(0,0,0,0.04))',
|
|
153
|
-
borderRadius: 6,
|
|
154
|
-
}}
|
|
155
|
-
>
|
|
156
|
-
{this.state.error.message}
|
|
157
|
-
</pre>
|
|
140
|
+
<pre className="shell-error-detail">{this.state.error.message}</pre>
|
|
158
141
|
<button
|
|
159
142
|
type="button"
|
|
160
143
|
className="btn sm"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Route } from '../hash-router.js'
|
|
2
2
|
import type { IconName } from '../icons.js'
|
|
3
|
-
import {
|
|
3
|
+
import { Icon } from '../icons.js'
|
|
4
4
|
import { navItems } from '../nav.js'
|
|
5
5
|
import { useWorkspace } from '../workspace-context.js'
|
|
6
6
|
import { SiteSwitcher } from './SiteSwitcher.js'
|
|
@@ -21,16 +21,10 @@ export function Sidebar(props: {
|
|
|
21
21
|
|
|
22
22
|
return (
|
|
23
23
|
<aside className="sidebar" aria-label="Main navigation" data-collapsed={props.collapsed}>
|
|
24
|
-
{/*
|
|
24
|
+
{/* Growth Labs gradient mark + Masthead wordmark. */}
|
|
25
25
|
<div className="sb-brand">
|
|
26
|
-
<div className="sb-mark"
|
|
27
|
-
|
|
28
|
-
</div>
|
|
29
|
-
{!props.collapsed && (
|
|
30
|
-
<div className="sb-wordmark">
|
|
31
|
-
Masthead<span className="dot">.</span>
|
|
32
|
-
</div>
|
|
33
|
-
)}
|
|
26
|
+
<div className="sb-mark" aria-hidden="true" />
|
|
27
|
+
{!props.collapsed && <div className="sb-wordmark">Masthead</div>}
|
|
34
28
|
</div>
|
|
35
29
|
|
|
36
30
|
{/* Workspace switcher */}
|
|
@@ -64,7 +58,7 @@ export function Sidebar(props: {
|
|
|
64
58
|
{/* Current-user footer */}
|
|
65
59
|
<div className="sb-foot">
|
|
66
60
|
<div className="sb-user">
|
|
67
|
-
<div className="avatar" style={{ background: 'var(--
|
|
61
|
+
<div className="avatar" style={{ background: 'var(--surface-inverse)' }}>
|
|
68
62
|
{initial}
|
|
69
63
|
</div>
|
|
70
64
|
{!props.collapsed && (
|
|
@@ -67,22 +67,11 @@ export function Topbar(props: {
|
|
|
67
67
|
style={{ position: 'relative' }}
|
|
68
68
|
>
|
|
69
69
|
<Icon name="bell" size={17} />
|
|
70
|
-
<span
|
|
71
|
-
style={{
|
|
72
|
-
position: 'absolute',
|
|
73
|
-
top: 7,
|
|
74
|
-
right: 8,
|
|
75
|
-
width: 7,
|
|
76
|
-
height: 7,
|
|
77
|
-
borderRadius: '50%',
|
|
78
|
-
background: 'var(--red)',
|
|
79
|
-
border: '1.5px solid var(--bg)',
|
|
80
|
-
}}
|
|
81
|
-
/>
|
|
70
|
+
<span className="notification-dot" />
|
|
82
71
|
</button>
|
|
83
72
|
|
|
84
73
|
{/* Divider */}
|
|
85
|
-
<div
|
|
74
|
+
<div className="topbar-divider" />
|
|
86
75
|
|
|
87
76
|
{/* New content quick-create */}
|
|
88
77
|
<button type="button" className="btn primary sm" onClick={props.onCommand}>
|