@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
@@ -1 +1 @@
1
- {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/ui/theme.ts"],"names":[],"mappings":"AAIA,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,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAEvE,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,SAAS,CA6BjD,CAAA;AACD,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,UAAU,EAAE,SAAS,CA6BtD,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;WACT,SAAS;YACJ,UAAU;aACV,OAAO;gBACR,UAAU;;CAE/B,CAAA;AAKD,wBAAgB,QAAQ,CAAC,YAAY,EAAE,OAAO,GAAG,MAAM,CAEtD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAU9F"}
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
- // src/ui/theme.ts theme tokens ported from the resolved Broadsheet design
2
- // (app.jsx). The accent/serif vars are applied to documentElement at runtime by
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
- 'Ink blue': {
6
- a: '#0c4a6e',
7
- s: '#075985',
8
- fg: '#ffffff',
9
- tint: 'rgba(12,74,110,0.09)',
10
- ring: 'rgba(12,74,110,0.38)',
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
- 'Ink blue': {
36
- a: '#4c9ae8',
37
- s: '#3b86d4',
38
- fg: '#06121d',
39
- tint: 'rgba(76,154,232,0.16)',
40
- ring: 'rgba(76,154,232,0.45)',
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: 'dark',
66
- accent: 'Ink blue',
22
+ theme: 'light',
23
+ accent: 'Growth Labs',
67
24
  density: 'regular',
68
25
  previewPos: 'off',
69
- serifDisplay: true,
26
+ serifDisplay: false,
70
27
  };
71
- const SERIF = '"Source Serif 4", Georgia, serif';
72
- const SANS = '"DM Sans", system-ui, sans-serif';
73
- export function serifVar(serifDisplay) {
74
- return serifDisplay ? SERIF : SANS;
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['Ink blue'];
37
+ const set = table[accent] ?? table['Growth Labs'];
79
38
  return {
80
39
  '--accent': set.a,
81
40
  '--accent-strong': set.s,
@@ -1 +1 @@
1
- {"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/ui/theme.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,gFAAgF;AAChF,yDAAyD;AAWzD,MAAM,CAAC,MAAM,OAAO,GAAkC;IACrD,UAAU,EAAE;QACX,CAAC,EAAE,SAAS;QACZ,CAAC,EAAE,SAAS;QACZ,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,sBAAsB;QAC5B,IAAI,EAAE,sBAAsB;KAC5B;IACD,WAAW,EAAE;QACZ,CAAC,EAAE,SAAS;QACZ,CAAC,EAAE,SAAS;QACZ,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,sBAAsB;QAC5B,IAAI,EAAE,sBAAsB;KAC5B;IACD,MAAM,EAAE;QACP,CAAC,EAAE,SAAS;QACZ,CAAC,EAAE,SAAS;QACZ,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,sBAAsB;QAC5B,IAAI,EAAE,sBAAsB;KAC5B;IACD,MAAM,EAAE;QACP,CAAC,EAAE,SAAS;QACZ,CAAC,EAAE,SAAS;QACZ,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,sBAAsB;QAC5B,IAAI,EAAE,sBAAsB;KAC5B;CACD,CAAA;AACD,MAAM,CAAC,MAAM,YAAY,GAAkC;IAC1D,UAAU,EAAE;QACX,CAAC,EAAE,SAAS;QACZ,CAAC,EAAE,SAAS;QACZ,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,uBAAuB;QAC7B,IAAI,EAAE,uBAAuB;KAC7B;IACD,WAAW,EAAE;QACZ,CAAC,EAAE,SAAS;QACZ,CAAC,EAAE,SAAS;QACZ,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,uBAAuB;QAC7B,IAAI,EAAE,uBAAuB;KAC7B;IACD,MAAM,EAAE;QACP,CAAC,EAAE,SAAS;QACZ,CAAC,EAAE,SAAS;QACZ,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,uBAAuB;QAC7B,IAAI,EAAE,uBAAuB;KAC7B;IACD,MAAM,EAAE;QACP,CAAC,EAAE,SAAS;QACZ,CAAC,EAAE,SAAS;QACZ,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,wBAAwB;QAC9B,IAAI,EAAE,wBAAwB;KAC9B;CACD,CAAA;AAMD,MAAM,CAAC,MAAM,cAAc,GAAG;IAC7B,KAAK,EAAE,MAAmB;IAC1B,MAAM,EAAE,UAAwB;IAChC,OAAO,EAAE,SAAoB;IAC7B,UAAU,EAAE,KAAmB;IAC/B,YAAY,EAAE,IAAI;CAClB,CAAA;AAED,MAAM,KAAK,GAAG,kCAAkC,CAAA;AAChD,MAAM,IAAI,GAAG,kCAAkC,CAAA;AAE/C,MAAM,UAAU,QAAQ,CAAC,YAAqB;IAC7C,OAAO,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;AACnC,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,MAAM,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,CAAA;IAC9C,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
+ {"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,CAuBA"}
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"}
@@ -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;AACxD,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;IAC5D,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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-labs/cms",
3
- "version": "0.5.26",
3
+ "version": "0.5.29",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "growth-labs-cms-migrations": "./dist/cli/migrations.js"
@@ -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(/['"`]/g, '')
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),
@@ -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
 
@@ -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 &quot;{route}&quot; screen mounts here.
105
- </div>
106
- )
102
+ return <div className="page shell-state">The &quot;{route}&quot; 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" style={{ padding: 40 }}>
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 { BrandMark, Icon } from '../icons.js'
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
- {/* Brand block mark + serif wordmark with accent period */}
24
+ {/* Growth Labs gradient mark + Masthead wordmark. */}
25
25
  <div className="sb-brand">
26
- <div className="sb-mark">
27
- <BrandMark size={18} color="#fff" />
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(--accent)' }}>
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 style={{ width: 1, height: 22, background: 'var(--border)', margin: '0 4px' }} />
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}>