@woodsportal/hubspot-kit 1.0.4 → 1.0.26

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 (217) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +4 -2
  3. package/dist/{chunk-JDGIAYHX.js → chunk-GTTIJVAR.js} +43 -3
  4. package/dist/chunk-GTTIJVAR.js.map +1 -0
  5. package/dist/cli.js +1400 -338
  6. package/dist/cli.js.map +1 -1
  7. package/dist/exec-U2OJATR3.js +12 -0
  8. package/dist/index.d.ts +20 -0
  9. package/dist/index.js +8 -1
  10. package/dist/index.js.map +1 -1
  11. package/examples/greenfield/module-hybrid-cdn/README.md +40 -3
  12. package/examples/greenfield/module-hybrid-cdn/public/fields.json +44 -1
  13. package/examples/greenfield/module-hybrid-cdn/src/app.tsx +8 -0
  14. package/examples/greenfield/module-hybrid-cdn/src/assets/css/main.dev.css +6 -1
  15. package/examples/greenfield/module-hybrid-cdn/src/cdn/app-entry.ts +32 -0
  16. package/examples/greenfield/module-hybrid-cdn/src/cdn/vendor-entry.ts +28 -0
  17. package/examples/greenfield/module-hybrid-cdn/src/components/layout/section-card.tsx +20 -0
  18. package/examples/greenfield/module-hybrid-cdn/src/components/layout/starter-nav.tsx +28 -0
  19. package/examples/greenfield/module-hybrid-cdn/src/components/layout/starter-shell.tsx +18 -0
  20. package/examples/greenfield/module-hybrid-cdn/src/config/router.ts +4 -0
  21. package/examples/greenfield/module-hybrid-cdn/src/config/routes.ts +5 -0
  22. package/examples/greenfield/module-hybrid-cdn/src/defaultData.ts +2 -7
  23. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-guide-page.tsx +93 -0
  24. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-home-page.tsx +21 -0
  25. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +52 -0
  26. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-single-page.tsx +17 -0
  27. package/examples/greenfield/module-hybrid-cdn/src/features/starter/content/woodscli-guide-content.ts +86 -0
  28. package/examples/greenfield/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.ts +47 -0
  29. package/examples/greenfield/module-hybrid-cdn/src/features/starter/index.ts +6 -0
  30. package/examples/greenfield/module-hybrid-cdn/src/hubspot/bootstrap.ts +10 -0
  31. package/examples/greenfield/module-hybrid-cdn/src/hubspot/default-data.ts +7 -0
  32. package/examples/greenfield/module-hybrid-cdn/src/integrations/tanstack-router/router.tsx +25 -0
  33. package/examples/greenfield/module-hybrid-cdn/src/main.tsx +4 -3
  34. package/examples/greenfield/module-hybrid-cdn/src/portal/mount-dev-preview.tsx +8 -0
  35. package/examples/greenfield/module-hybrid-cdn/src/portal/mount-portal.tsx +12 -0
  36. package/examples/greenfield/module-hybrid-cdn/src/routeTree.gen.ts +77 -0
  37. package/examples/greenfield/module-hybrid-cdn/src/routes/__root.tsx +15 -0
  38. package/examples/greenfield/module-hybrid-cdn/src/routes/guide.tsx +7 -0
  39. package/examples/greenfield/module-hybrid-cdn/src/routes/index.tsx +7 -0
  40. package/examples/greenfield/module-hybrid-cdn/tsconfig.json +20 -0
  41. package/examples/greenfield/module-monolith/public/fields.json +44 -1
  42. package/examples/greenfield/module-monolith/src/app.tsx +8 -0
  43. package/examples/greenfield/module-monolith/src/assets/css/main.dev.css +8 -0
  44. package/examples/greenfield/module-monolith/src/components/layout/section-card.tsx +20 -0
  45. package/examples/greenfield/module-monolith/src/components/layout/starter-nav.tsx +28 -0
  46. package/examples/greenfield/module-monolith/src/components/layout/starter-shell.tsx +18 -0
  47. package/examples/greenfield/module-monolith/src/config/router.ts +4 -0
  48. package/examples/greenfield/module-monolith/src/config/routes.ts +5 -0
  49. package/examples/greenfield/module-monolith/src/defaultData.ts +2 -7
  50. package/examples/greenfield/module-monolith/src/features/starter/components/starter-guide-page.tsx +93 -0
  51. package/examples/greenfield/module-monolith/src/features/starter/components/starter-home-page.tsx +21 -0
  52. package/examples/greenfield/module-monolith/src/features/starter/components/starter-home.tsx +52 -0
  53. package/examples/greenfield/module-monolith/src/features/starter/components/starter-single-page.tsx +17 -0
  54. package/examples/greenfield/module-monolith/src/features/starter/content/woodscli-guide-content.ts +86 -0
  55. package/examples/greenfield/module-monolith/src/features/starter/hooks/use-starter-preview.ts +47 -0
  56. package/examples/greenfield/module-monolith/src/features/starter/index.ts +6 -0
  57. package/examples/greenfield/module-monolith/src/hubspot/default-data.ts +7 -0
  58. package/examples/greenfield/module-monolith/src/hubspot/types.ts +17 -0
  59. package/examples/greenfield/module-monolith/src/integrations/tanstack-router/router.tsx +25 -0
  60. package/examples/greenfield/module-monolith/src/main.tsx +5 -0
  61. package/examples/greenfield/module-monolith/src/portal/mount-dev-preview.tsx +8 -0
  62. package/examples/greenfield/module-monolith/src/portal/mount-portal.tsx +12 -0
  63. package/examples/greenfield/module-monolith/src/routeTree.gen.ts +77 -0
  64. package/examples/greenfield/module-monolith/src/routes/__root.tsx +15 -0
  65. package/examples/greenfield/module-monolith/src/routes/guide.tsx +7 -0
  66. package/examples/greenfield/module-monolith/src/routes/index.tsx +7 -0
  67. package/examples/greenfield/module-monolith/tsconfig.json +20 -0
  68. package/package.json +4 -1
  69. package/scripts/check-hubspot-bundle-size.mjs +5 -2
  70. package/scripts/ensure-wphs-workspace.mjs +2 -1
  71. package/scripts/hubspot-module-postbuild.mjs +1 -1
  72. package/scripts/read-woodsportal-config.mjs +4 -1
  73. package/scripts/run-hubspot-module-postbuild.mjs +9 -4
  74. package/scripts/vite/bootstrap-shim-resolver.js +47 -0
  75. package/scripts/vite/ensure-dev-kit-shims.js +7 -1
  76. package/scripts/vite/react-dev-plugin-options.js +2 -1
  77. package/scripts/watch-hubspot.mjs +107 -17
  78. package/scripts/wphs-paths.mjs +1 -0
  79. package/scripts/write-audit-artifacts.mjs +2 -1
  80. package/src/dev/bootstrap-greenfield.tsx +59 -0
  81. package/src/dev/bootstrap.tsx +46 -0
  82. package/src/dev-module-config/__tests__/fixtures/fields.json +3014 -0
  83. package/src/dev-module-config/__tests__/fixtures/golden-module.fixture.ts +102 -0
  84. package/src/dev-module-config/__tests__/module-config-storage.test.ts +40 -0
  85. package/src/dev-module-config/__tests__/module-config-transform.test.ts +55 -0
  86. package/src/dev-module-config/__tests__/normalize-repeater-items.test.ts +46 -0
  87. package/src/dev-module-config/dev-module-display-name.ts +5 -0
  88. package/src/dev-module-config/runtime/module-config-bridge.ts +27 -0
  89. package/src/dev-module-config/runtime/module-config-runtime.ts +185 -0
  90. package/src/dev-module-config/schema/build-default-item.ts +7 -0
  91. package/src/dev-module-config/schema/deep-merge.ts +27 -0
  92. package/src/dev-module-config/schema/extract-defaults.ts +62 -0
  93. package/src/dev-module-config/schema/normalize-choices.ts +18 -0
  94. package/src/dev-module-config/schema/normalize-repeater-items.ts +31 -0
  95. package/src/dev-module-config/schema/parse-fields-json.ts +2 -0
  96. package/src/dev-module-config/schema/types.ts +75 -0
  97. package/src/dev-module-config/schema/validate-module-config.ts +81 -0
  98. package/src/dev-module-config/schema/visibility.ts +130 -0
  99. package/src/dev-module-config/storage/config-json.ts +46 -0
  100. package/src/dev-module-config/storage/workspace-store.ts +139 -0
  101. package/src/dev-module-config/transform/build-hubspot-data.ts +409 -0
  102. package/src/dev-module-config/ui/FieldRenderer.tsx +353 -0
  103. package/src/dev-module-config/ui/ModuleConfigAnimatedPresence.tsx +72 -0
  104. package/src/dev-module-config/ui/ModuleConfigCollapsible.tsx +43 -0
  105. package/src/dev-module-config/ui/ModuleConfigDockSideIcon.tsx +27 -0
  106. package/src/dev-module-config/ui/ModuleConfigOverlay.tsx +134 -0
  107. package/src/dev-module-config/ui/ModuleConfigPersistenceStatus.tsx +89 -0
  108. package/src/dev-module-config/ui/ModuleConfigShell.tsx +355 -0
  109. package/src/dev-module-config/ui/ModuleConfigThemeToggle.tsx +32 -0
  110. package/src/dev-module-config/ui/ModuleConfigUiContext.tsx +65 -0
  111. package/src/dev-module-config/ui/editors/DeveloperOptionsEditor.tsx +61 -0
  112. package/src/dev-module-config/ui/editors/MenuOptionsEditor.tsx +103 -0
  113. package/src/dev-module-config/ui/hubspot/HsColorField.tsx +106 -0
  114. package/src/dev-module-config/ui/hubspot/HsColorPickerPopover.tsx +210 -0
  115. package/src/dev-module-config/ui/hubspot/HsFieldLabel.tsx +24 -0
  116. package/src/dev-module-config/ui/hubspot/HsFieldWidgets.tsx +248 -0
  117. package/src/dev-module-config/ui/hubspot/HsImageField.tsx +112 -0
  118. package/src/dev-module-config/ui/hubspot/HsOccurrenceGroupEditor.tsx +243 -0
  119. package/src/dev-module-config/ui/hubspot/HsRepeaterList.tsx +178 -0
  120. package/src/dev-module-config/ui/hubspot/HsSectionHeader.tsx +20 -0
  121. package/src/dev-module-config/ui/hubspot/HsToggle.tsx +39 -0
  122. package/src/dev-module-config/ui/hubspot/HsToggleRow.tsx +30 -0
  123. package/src/dev-module-config/ui/hubspot/hs-color-palette.ts +11 -0
  124. package/src/dev-module-config/ui/hubspot/module-config-typography.ts +26 -0
  125. package/src/dev-module-config/ui/module-config-motion.css +134 -0
  126. package/src/dev-module-config/ui/module-config-motion.ts +28 -0
  127. package/src/dev-module-config/ui/module-config-overlay-side.ts +29 -0
  128. package/src/dev-module-config/ui/module-config-surfaces.ts +27 -0
  129. package/src/dev-module-config/ui/module-config-theme.css +489 -0
  130. package/src/dev-module-config/ui/module-config-toolbar-theme.ts +35 -0
  131. package/src/dev-module-config/ui/navigation/ModuleConfigNavContext.tsx +194 -0
  132. package/src/dev-module-config/ui/use-dev-module-config-live-revision.ts +22 -0
  133. package/src/dev-module-config/ui/use-hubspot-live-exports.ts +27 -0
  134. package/src/dev-module-config/ui/use-module-config.ts +124 -0
  135. package/src/routing/build-api-routes.ts +37 -0
  136. package/src/routing/route-format.ts +19 -0
  137. package/templates/module-hybrid-cdn/README.md +53 -3
  138. package/templates/module-hybrid-cdn/index.html +12 -1
  139. package/templates/module-hybrid-cdn/package.json +6 -2
  140. package/templates/module-hybrid-cdn/public/fields.json +44 -1
  141. package/templates/module-hybrid-cdn/src/app.tsx +8 -0
  142. package/templates/module-hybrid-cdn/src/assets/css/main.dev.css +6 -1
  143. package/templates/module-hybrid-cdn/src/cdn/app-entry.ts +32 -0
  144. package/templates/module-hybrid-cdn/src/cdn/vendor-entry.ts +28 -0
  145. package/templates/module-hybrid-cdn/src/components/layout/section-card.tsx +20 -0
  146. package/templates/module-hybrid-cdn/src/components/layout/starter-nav.tsx +28 -0
  147. package/templates/module-hybrid-cdn/src/components/layout/starter-shell.tsx +18 -0
  148. package/templates/module-hybrid-cdn/src/config/router.ts +4 -0
  149. package/templates/module-hybrid-cdn/src/config/routes.ts +5 -0
  150. package/templates/module-hybrid-cdn/src/defaultData.ts +2 -7
  151. package/templates/module-hybrid-cdn/src/features/starter/components/starter-guide-page.tsx +93 -0
  152. package/templates/module-hybrid-cdn/src/features/starter/components/starter-home-page.tsx +21 -0
  153. package/templates/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +52 -0
  154. package/templates/module-hybrid-cdn/src/features/starter/components/starter-single-page.tsx +17 -0
  155. package/templates/module-hybrid-cdn/src/features/starter/content/woodscli-guide-content.ts +86 -0
  156. package/templates/module-hybrid-cdn/src/features/starter/hooks/starter-preview-values.ts +42 -0
  157. package/templates/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.dev.ts +17 -0
  158. package/templates/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.ts +30 -0
  159. package/templates/module-hybrid-cdn/src/features/starter/index.ts +6 -0
  160. package/templates/module-hybrid-cdn/src/hubspot/bootstrap.ts +10 -0
  161. package/templates/module-hybrid-cdn/src/hubspot/default-data.ts +7 -0
  162. package/templates/module-hybrid-cdn/src/hubspot/module-entry.tsx +9 -0
  163. package/templates/module-hybrid-cdn/src/integrations/tanstack-router/router.tsx +25 -0
  164. package/templates/module-hybrid-cdn/src/main.tsx +4 -3
  165. package/templates/module-hybrid-cdn/src/portal/mount-dev-preview.tsx +8 -0
  166. package/templates/module-hybrid-cdn/src/portal/mount-portal.tsx +12 -0
  167. package/templates/module-hybrid-cdn/src/routeTree.gen.ts +77 -0
  168. package/templates/module-hybrid-cdn/src/routes/__root.tsx +15 -0
  169. package/templates/module-hybrid-cdn/src/routes/guide.tsx +7 -0
  170. package/templates/module-hybrid-cdn/src/routes/index.tsx +7 -0
  171. package/templates/module-hybrid-cdn/tsconfig.json +20 -0
  172. package/templates/module-hybrid-cdn/vite.config.monolith.mjs +8 -0
  173. package/templates/module-monolith/README.md +59 -0
  174. package/templates/module-monolith/index.html +12 -0
  175. package/templates/module-monolith/package.json +10 -3
  176. package/templates/module-monolith/public/fields.json +44 -1
  177. package/templates/module-monolith/public/module.html +1 -0
  178. package/templates/module-monolith/src/app.tsx +8 -0
  179. package/templates/module-monolith/src/assets/css/main.dev.css +6 -1
  180. package/templates/module-monolith/src/components/layout/section-card.tsx +20 -0
  181. package/templates/module-monolith/src/components/layout/starter-nav.tsx +28 -0
  182. package/templates/module-monolith/src/components/layout/starter-shell.tsx +18 -0
  183. package/templates/module-monolith/src/config/router.ts +4 -0
  184. package/templates/module-monolith/src/config/routes.ts +5 -0
  185. package/templates/module-monolith/src/defaultData.ts +2 -7
  186. package/templates/module-monolith/src/features/starter/components/starter-guide-page.tsx +93 -0
  187. package/templates/module-monolith/src/features/starter/components/starter-home-page.tsx +21 -0
  188. package/templates/module-monolith/src/features/starter/components/starter-home.tsx +52 -0
  189. package/templates/module-monolith/src/features/starter/components/starter-single-page.tsx +17 -0
  190. package/templates/module-monolith/src/features/starter/content/woodscli-guide-content.ts +86 -0
  191. package/templates/module-monolith/src/features/starter/hooks/starter-preview-values.ts +42 -0
  192. package/templates/module-monolith/src/features/starter/hooks/use-starter-preview.dev.ts +17 -0
  193. package/templates/module-monolith/src/features/starter/hooks/use-starter-preview.ts +30 -0
  194. package/templates/module-monolith/src/features/starter/index.ts +6 -0
  195. package/templates/module-monolith/src/hubspot/default-data.ts +7 -0
  196. package/templates/module-monolith/src/hubspot/module-entry.tsx +9 -0
  197. package/templates/module-monolith/src/integrations/tanstack-router/router.tsx +25 -0
  198. package/templates/module-monolith/src/main.tsx +4 -3
  199. package/templates/module-monolith/src/portal/mount-dev-preview.tsx +8 -0
  200. package/templates/module-monolith/src/portal/mount-portal.tsx +12 -0
  201. package/templates/module-monolith/src/routeTree.gen.ts +77 -0
  202. package/templates/module-monolith/src/routes/__root.tsx +15 -0
  203. package/templates/module-monolith/src/routes/guide.tsx +7 -0
  204. package/templates/module-monolith/src/routes/index.tsx +7 -0
  205. package/templates/module-monolith/tsconfig.json +20 -0
  206. package/templates/module-monolith/vite.config.monolith.mjs +8 -0
  207. package/templates/module-monolith/woodscli.json +1 -0
  208. package/templates/shared/.env.dev.example +5 -0
  209. package/templates/shared/.env.prod.example +5 -0
  210. package/templates/shared/.env.stg.example +3 -0
  211. package/templates/shared/portal.cdn.json.example +8 -0
  212. package/templates/theme-starter/package.json +5 -2
  213. package/vite/vite.config.dev.mjs +9 -1
  214. package/vite/vite.config.monolith.mjs +22 -17
  215. package/dist/chunk-JDGIAYHX.js.map +0 -1
  216. package/dist/exec-K5C6LZP4.js +0 -10
  217. /package/dist/{exec-K5C6LZP4.js.map → exec-U2OJATR3.js.map} +0 -0
@@ -0,0 +1,35 @@
1
+ export type ModuleConfigToolbarTheme = 'light' | 'dark'
2
+
3
+ const STORAGE_KEY = 'woodsportal:module-config-toolbar-theme'
4
+
5
+ export function detectPortalTheme(): ModuleConfigToolbarTheme {
6
+ if (typeof document === 'undefined') return 'light'
7
+ return document.documentElement.classList.contains('dark') ? 'dark' : 'light'
8
+ }
9
+
10
+ export function loadModuleConfigToolbarTheme(): ModuleConfigToolbarTheme {
11
+ if (typeof window === 'undefined') return 'light'
12
+ try {
13
+ const stored = window.localStorage.getItem(STORAGE_KEY)
14
+ if (stored === 'dark' || stored === 'light') return stored
15
+ } catch {
16
+ // ignore
17
+ }
18
+ return detectPortalTheme()
19
+ }
20
+
21
+ export function saveModuleConfigToolbarTheme(theme: ModuleConfigToolbarTheme): void {
22
+ try {
23
+ window.localStorage.setItem(STORAGE_KEY, theme)
24
+ } catch {
25
+ // ignore
26
+ }
27
+ }
28
+
29
+ export function toggleModuleConfigToolbarTheme(
30
+ theme: ModuleConfigToolbarTheme,
31
+ ): ModuleConfigToolbarTheme {
32
+ const next: ModuleConfigToolbarTheme = theme === 'dark' ? 'light' : 'dark'
33
+ saveModuleConfigToolbarTheme(next)
34
+ return next
35
+ }
@@ -0,0 +1,194 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useMemo,
6
+ useState,
7
+ type ReactNode,
8
+ } from 'react'
9
+
10
+ import { devModuleDisplayName } from '../../dev-module-display-name'
11
+
12
+ export type BreadcrumbSegment = {
13
+ id: string
14
+ label: string
15
+ onClick?: () => void
16
+ }
17
+
18
+ type ModuleConfigNavContextValue = {
19
+ segments: Array<BreadcrumbSegment>
20
+ drillDownPanel: ReactNode | null
21
+ pushSegment: (segment: Omit<BreadcrumbSegment, 'onClick'> & { onPop?: () => void }) => void
22
+ popTo: (id: string) => void
23
+ resetNav: () => void
24
+ registerPopHandler: (id: string, handler: () => void) => void
25
+ /** @deprecated use registerDrillDownPanel */
26
+ setDrillDownPanel: (panel: ReactNode | null) => void
27
+ registerDrillDownPanel: (id: string, panel: ReactNode | null) => void
28
+ }
29
+
30
+ const ROOT_SEGMENT: BreadcrumbSegment = { id: 'contents', label: 'Contents' }
31
+
32
+ function moduleTitleSegment(stackLength: number, popTo: (id: string) => void): BreadcrumbSegment {
33
+ return {
34
+ id: 'module',
35
+ label: devModuleDisplayName(),
36
+ onClick: stackLength > 1 ? () => popTo('module') : undefined,
37
+ }
38
+ }
39
+
40
+ const ModuleConfigNavContext = createContext<ModuleConfigNavContextValue | null>(null)
41
+
42
+ export function ModuleConfigNavProvider({ children }: { children: ReactNode }) {
43
+ const [stack, setStack] = useState<Array<BreadcrumbSegment>>([ROOT_SEGMENT])
44
+ const [popHandlers, setPopHandlers] = useState<Record<string, () => void>>({})
45
+ const [drillDownPanels, setDrillDownPanels] = useState<Record<string, ReactNode>>({})
46
+ const [drillDownOrder, setDrillDownOrder] = useState<string[]>([])
47
+
48
+ const registerDrillDownPanel = useCallback((id: string, panel: ReactNode | null) => {
49
+ if (panel === null) {
50
+ setDrillDownPanels((prev) => {
51
+ if (!(id in prev)) return prev
52
+ const next = { ...prev }
53
+ delete next[id]
54
+ return next
55
+ })
56
+ setDrillDownOrder((order) => order.filter((entry) => entry !== id))
57
+ return
58
+ }
59
+
60
+ setDrillDownPanels((prev) => ({ ...prev, [id]: panel }))
61
+ setDrillDownOrder((order) => (order.includes(id) ? order : [...order, id]))
62
+ }, [])
63
+
64
+ const clearAllDrillDownPanels = useCallback(() => {
65
+ setDrillDownPanels({})
66
+ setDrillDownOrder([])
67
+ }, [])
68
+
69
+ const drillDownPanel = useMemo(() => {
70
+ const topId = drillDownOrder[drillDownOrder.length - 1]
71
+ return topId ? (drillDownPanels[topId] ?? null) : null
72
+ }, [drillDownOrder, drillDownPanels])
73
+
74
+ const setDrillDownPanel = useCallback(
75
+ (panel: ReactNode | null) => {
76
+ if (panel === null) {
77
+ clearAllDrillDownPanels()
78
+ return
79
+ }
80
+ registerDrillDownPanel('__legacy__', panel)
81
+ },
82
+ [clearAllDrillDownPanels, registerDrillDownPanel],
83
+ )
84
+
85
+ const registerPopHandler = useCallback((id: string, handler: () => void) => {
86
+ setPopHandlers((prev) => ({ ...prev, [id]: handler }))
87
+ }, [])
88
+
89
+ const popTo = useCallback(
90
+ (id: string) => {
91
+ if (id === 'module' || id === 'contents') {
92
+ for (const segment of stack.slice(1)) {
93
+ popHandlers[segment.id]?.()
94
+ }
95
+ setStack([ROOT_SEGMENT])
96
+ clearAllDrillDownPanels()
97
+ return
98
+ }
99
+
100
+ setStack((prev) => {
101
+ const index = prev.findIndex((segment) => segment.id === id)
102
+ if (index < 0) return prev
103
+
104
+ const removed = prev.slice(index + 1)
105
+ for (const segment of removed) {
106
+ popHandlers[segment.id]?.()
107
+ }
108
+
109
+ return prev.slice(0, index + 1)
110
+ })
111
+ },
112
+ [clearAllDrillDownPanels, popHandlers, stack],
113
+ )
114
+
115
+ const pushSegment = useCallback(
116
+ (segment: Omit<BreadcrumbSegment, 'onClick'> & { onPop?: () => void }) => {
117
+ if (segment.onPop) {
118
+ setPopHandlers((prev) => ({ ...prev, [segment.id]: segment.onPop! }))
119
+ }
120
+ setStack((prev) => {
121
+ const existing = prev.findIndex((item) => item.id === segment.id)
122
+ if (existing >= 0) {
123
+ return [...prev.slice(0, existing + 1), { id: segment.id, label: segment.label }]
124
+ }
125
+ return [...prev, { id: segment.id, label: segment.label }]
126
+ })
127
+ },
128
+ [],
129
+ )
130
+
131
+ const resetNav = useCallback(() => {
132
+ setStack([ROOT_SEGMENT])
133
+ clearAllDrillDownPanels()
134
+ }, [clearAllDrillDownPanels])
135
+
136
+ const segments = useMemo(() => {
137
+ const moduleTitle = moduleTitleSegment(stack.length, popTo)
138
+
139
+ if (stack.length <= 1) {
140
+ return [ROOT_SEGMENT, moduleTitle]
141
+ }
142
+
143
+ const middle = stack.slice(1).map((segment) => ({
144
+ ...segment,
145
+ onClick: () => popTo(segment.id),
146
+ }))
147
+
148
+ if (middle.length > 2) {
149
+ const collapsed: Array<BreadcrumbSegment> = [
150
+ middle[0]!,
151
+ { id: 'ellipsis', label: '…' },
152
+ middle[middle.length - 1]!,
153
+ ]
154
+ return [ROOT_SEGMENT, moduleTitle, ...collapsed]
155
+ }
156
+
157
+ return [ROOT_SEGMENT, moduleTitle, ...middle]
158
+ }, [popTo, stack])
159
+
160
+ const value = useMemo(
161
+ () => ({
162
+ segments,
163
+ drillDownPanel,
164
+ pushSegment,
165
+ popTo,
166
+ resetNav,
167
+ registerPopHandler,
168
+ setDrillDownPanel,
169
+ registerDrillDownPanel,
170
+ }),
171
+ [
172
+ segments,
173
+ drillDownPanel,
174
+ pushSegment,
175
+ popTo,
176
+ resetNav,
177
+ registerPopHandler,
178
+ setDrillDownPanel,
179
+ registerDrillDownPanel,
180
+ ],
181
+ )
182
+
183
+ return (
184
+ <ModuleConfigNavContext.Provider value={value}>{children}</ModuleConfigNavContext.Provider>
185
+ )
186
+ }
187
+
188
+ export function useModuleConfigNav() {
189
+ const ctx = useContext(ModuleConfigNavContext)
190
+ if (!ctx) {
191
+ throw new Error('useModuleConfigNav must be used within ModuleConfigNavProvider')
192
+ }
193
+ return ctx
194
+ }
@@ -0,0 +1,22 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { MODULE_CONFIG_CHANGED } from '../schema/types'
3
+
4
+ /**
5
+ * DEV-only: bump when module config is applied so the portal can re-read hubSpotData exports.
6
+ */
7
+ export function useDevModuleConfigLiveRevision(): number {
8
+ const [revision, setRevision] = useState(0)
9
+
10
+ useEffect(() => {
11
+ if (!import.meta.env.DEV) return
12
+
13
+ const onApplied = () => {
14
+ setRevision((value) => value + 1)
15
+ }
16
+
17
+ window.addEventListener(MODULE_CONFIG_CHANGED, onApplied)
18
+ return () => window.removeEventListener(MODULE_CONFIG_CHANGED, onApplied)
19
+ }, [])
20
+
21
+ return revision
22
+ }
@@ -0,0 +1,27 @@
1
+ import { useMemo } from 'react'
2
+ import { getDevHubSpotLiveExports, type DevHubSpotLiveExports } from '@/data/dev-hubspot-sync'
3
+ import { refreshHubSpotUserDetails } from '@/data/hubSpotData'
4
+ import { useDevModuleConfigLiveRevision } from './use-dev-module-config-live-revision'
5
+
6
+ /**
7
+ * DEV-only: re-read hubSpotData exports after module config Apply.
8
+ * Production callers receive revision 0 and should keep using module-level imports.
9
+ */
10
+ export function useHubSpotLiveExports(): {
11
+ revision: number
12
+ live: DevHubSpotLiveExports | null
13
+ } {
14
+ const revision = useDevModuleConfigLiveRevision()
15
+
16
+ return useMemo(() => {
17
+ if (!import.meta.env.DEV) {
18
+ return { revision: 0, live: null }
19
+ }
20
+
21
+ refreshHubSpotUserDetails()
22
+ return {
23
+ revision,
24
+ live: getDevHubSpotLiveExports(),
25
+ }
26
+ }, [revision])
27
+ }
@@ -0,0 +1,124 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import {
3
+ getModuleConfigState,
4
+ getModuleFieldValues,
5
+ initModuleConfigRuntime,
6
+ updateModuleFieldValues,
7
+ } from '../runtime/module-config-runtime'
8
+ import { validateModuleConfig, type ValidationError } from '../schema/validate-module-config'
9
+ import { loadWorkspaceMeta } from '../storage/workspace-store'
10
+ import { MODULE_CONFIG_CHANGED } from '../schema/types'
11
+ import type { ModuleConfigState } from '../runtime/module-config-runtime'
12
+ import type { ModuleFieldValues } from '../schema/types'
13
+
14
+ export function useModuleConfig() {
15
+ const [state, setState] = useState<ModuleConfigState | null>(() => getModuleConfigState())
16
+ const [draftValues, setDraftValues] = useState<ModuleFieldValues>(() => getModuleFieldValues())
17
+ const [appliedValues, setAppliedValues] = useState<ModuleFieldValues>(() => getModuleFieldValues())
18
+ const [validationErrors, setValidationErrors] = useState<Array<ValidationError>>([])
19
+ const [persistenceSource, setPersistenceSource] = useState<'local' | 'example' | 'empty'>('empty')
20
+ const [isApplying, setIsApplying] = useState(false)
21
+
22
+ const isDirty =
23
+ JSON.stringify(draftValues) !== JSON.stringify(appliedValues)
24
+
25
+ useEffect(() => {
26
+ void loadWorkspaceMeta().then((source) => setPersistenceSource(source ?? 'empty'))
27
+ }, [appliedValues])
28
+
29
+ useEffect(() => {
30
+ const onChange = (event: Event) => {
31
+ const detail = (event as CustomEvent<ModuleConfigState>).detail
32
+ setState(detail)
33
+ setDraftValues(detail.moduleValues)
34
+ setAppliedValues(detail.moduleValues)
35
+ setValidationErrors([])
36
+ }
37
+ window.addEventListener(MODULE_CONFIG_CHANGED, onChange)
38
+ return () => window.removeEventListener(MODULE_CONFIG_CHANGED, onChange)
39
+ }, [])
40
+
41
+ const setFieldValue = useCallback((path: string, value: unknown) => {
42
+ setDraftValues((current) => {
43
+ const next = structuredClone(current)
44
+ const parts = path.split('.')
45
+ let cursor: Record<string, unknown> = next
46
+ for (let i = 0; i < parts.length - 1; i++) {
47
+ const key = parts[i]!
48
+ if (cursor[key] === undefined || cursor[key] === null) cursor[key] = {}
49
+ cursor = cursor[key] as Record<string, unknown>
50
+ }
51
+ cursor[parts[parts.length - 1]!] = value
52
+ return next
53
+ })
54
+ setValidationErrors((errors) => errors.filter((error) => !error.path.startsWith(path)))
55
+ }, [])
56
+
57
+ const applyChanges = useCallback(async (): Promise<boolean> => {
58
+ const currentState = getModuleConfigState()
59
+ if (!currentState) return false
60
+
61
+ const errors = validateModuleConfig(currentState.schema, draftValues)
62
+ if (errors.length > 0) {
63
+ setValidationErrors(errors)
64
+ return false
65
+ }
66
+
67
+ setIsApplying(true)
68
+ try {
69
+ const updated = await updateModuleFieldValues(draftValues)
70
+ setState(updated)
71
+ setDraftValues(updated.moduleValues)
72
+ setAppliedValues(updated.moduleValues)
73
+ setValidationErrors([])
74
+ const source = await loadWorkspaceMeta()
75
+ setPersistenceSource(source ?? 'local')
76
+ return true
77
+ } catch (error) {
78
+ console.error('[module-config] Apply failed', error)
79
+ setValidationErrors([
80
+ {
81
+ path: '_apply',
82
+ label: 'Apply',
83
+ message:
84
+ error instanceof Error
85
+ ? error.message
86
+ : 'Failed to save config. Restart yarn local if the dev server was started before the module-config plugin was added.',
87
+ },
88
+ ])
89
+ return false
90
+ } finally {
91
+ setIsApplying(false)
92
+ }
93
+ }, [draftValues])
94
+
95
+ const discardDraft = useCallback(() => {
96
+ setDraftValues(appliedValues)
97
+ setValidationErrors([])
98
+ }, [appliedValues])
99
+
100
+ const confirmDiscardIfDirty = useCallback((): boolean => {
101
+ if (!isDirty) return true
102
+ return window.confirm('You have unsaved changes. Discard them?')
103
+ }, [isDirty])
104
+
105
+ return {
106
+ state,
107
+ moduleValues: draftValues,
108
+ appliedValues,
109
+ isDirty,
110
+ isApplying,
111
+ validationErrors,
112
+ persistenceSource,
113
+ setFieldValue,
114
+ applyChanges,
115
+ discardDraft,
116
+ confirmDiscardIfDirty,
117
+ }
118
+ }
119
+
120
+ export async function ensureModuleConfigReady() {
121
+ if (!getModuleConfigState()) {
122
+ await initModuleConfigRuntime()
123
+ }
124
+ }
@@ -0,0 +1,37 @@
1
+ import { formatCustomObjectLabel, formatPath } from './route-format'
2
+
3
+ export const makeLink = (menuItem: Record<string, unknown>) => {
4
+ let link = menuItem?.hubspotObjectTypeId as string | undefined
5
+
6
+ if (menuItem?.companyAsMediator) {
7
+ link = `${link}-c`
8
+ }
9
+
10
+ if (menuItem?.specPipeLine && menuItem?.pipeLineId) {
11
+ link = `${link}-${menuItem?.pipeLineId}`
12
+ }
13
+
14
+ if (!link) {
15
+ return `/${formatPath((menuItem.tabName || menuItem.label) as string)}`
16
+ }
17
+
18
+ return `/${link}`
19
+ }
20
+
21
+ export function buildApiRoutes(sidebarMenuOptions: Array<Record<string, unknown>>) {
22
+ return sidebarMenuOptions.map((menuItem) => ({
23
+ hubspotObjectTypeId: `${menuItem.hubspotObjectTypeId ?? ''}`,
24
+ path: makeLink(menuItem),
25
+ title: formatCustomObjectLabel((menuItem.tabName || menuItem.label) as string),
26
+ icon: menuItem.icon,
27
+ isRequiredAuth: true,
28
+ isHeader: true,
29
+ companyAsMediator: menuItem.companyAsMediator,
30
+ pipeLineId: menuItem.pipeLineId,
31
+ specPipeLine: menuItem.specPipeLine,
32
+ objectDescription: menuItem.objectDescription,
33
+ objectUserProperties: menuItem.objectUserProperties,
34
+ homeCardsView: menuItem.homeCardsView,
35
+ objectUserPropertiesView: menuItem.objectUserPropertiesView,
36
+ }))
37
+ }
@@ -0,0 +1,19 @@
1
+ /** Route path / label helpers — runtime-safe, no consumer path aliases. */
2
+
3
+ export const formatPath = (key: unknown) => {
4
+ if (!key || typeof key !== 'string') return ''
5
+
6
+ return key
7
+ .replace(/\s+/g, '-')
8
+ .replace(/#/g, '%23')
9
+ .replace(/\b\w/g, (l: string) => l.toLowerCase())
10
+ }
11
+
12
+ export function formatCustomObjectLabel(key = '') {
13
+ if (!key || typeof key !== 'string') return ''
14
+
15
+ return key
16
+ .replace(/-/g, ' ')
17
+ .replace(/%23/g, '#')
18
+ .replace(/\b[a-z]/g, (l: string) => l.toUpperCase())
19
+ }
@@ -1,9 +1,59 @@
1
- # HubSpot portal module (hybrid CDN)
1
+ # Getting started
2
2
 
3
- Scaffolded by `wp init --template module-hybrid-cdn`.
3
+ This project was scaffolded with **WoodsCLI** (`@woodsportal/hubspot-kit`).
4
+
5
+ ## Daily dev
4
6
 
5
7
  ```bash
6
- npm install
8
+ yarn install
7
9
  wp doctor
8
10
  wp dev
9
11
  ```
12
+
13
+ Open the local URL, then **Ctrl+Shift+Z** (Mac **⌃⇧Z**) to edit module fields. Click **Apply changes** to refresh the starter preview.
14
+
15
+ ## Routing (`wp init`)
16
+
17
+ Default is **hash history** — same as `woodsportal-client-frontend` (`#/guide` URLs work in HubSpot CMS).
18
+
19
+ | `wp init --router` | Behavior |
20
+ |--------------------|----------|
21
+ | `hash` (default) | `createHashHistory()` — production-safe in HubSpot embeds |
22
+ | `browser` | Clean `/guide` URLs for local dev only |
23
+ | `none` | Single-page starter, no TanStack Router |
24
+
25
+ Recorded in `woodscli.json` → `scaffold.router`.
26
+
27
+ ## `src/` layout
28
+
29
+ | Path | Purpose |
30
+ |------|---------|
31
+ | `main.tsx` | Dev entry — loads styles and `mountDevPreview()` |
32
+ | `app.tsx` | `RouterProvider` shell (client-frontend pattern) |
33
+ | `config/router.ts` | Hash vs browser history |
34
+ | `portal/` | Dev vs production mount (`mount-dev-preview`, `mount-portal`) |
35
+ | `routes/` | TanStack file routes — keep thin; import from `features/` |
36
+ | `features/starter/` | Demo home + developer guide (replace with your app) |
37
+ | `components/` | Shared layout and UI |
38
+ | `integrations/` | Router and third-party wiring |
39
+ | `hubspot/` | HubSpot types and dev fixtures |
40
+ | `config/` | Route constants and app config |
41
+ | `assets/css/` | Tailwind entry for local dev |
42
+
43
+ Kit runtime expects `src/defaultData.ts` (re-exports `hubspot/default-data.ts`).
44
+
45
+ ## Docs (in the kit package)
46
+
47
+ After `npm install`, see:
48
+
49
+ - `node_modules/@woodsportal/hubspot-kit/docs/CLI-REFERENCE.md`
50
+ - `node_modules/@woodsportal/hubspot-kit/docs/MODULES.md`
51
+ - `node_modules/@woodsportal/hubspot-kit/docs/HUBSPOT-CLI-SETUP.md`
52
+
53
+ ## HubSpot upload
54
+
55
+ ```bash
56
+ cp hubspot.config.yml.example hubspot.config.yml
57
+ # add portal + personal access key
58
+ wp upload
59
+ ```
@@ -1 +1,12 @@
1
- <!doctype html><html><head><meta charset="UTF-8"/><title>WoodsCLI dev</title></head><body><div id="app"></div><script type="module" src="/src/main.tsx"></script></body></html>
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>WoodsCLI dev</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -9,11 +9,15 @@
9
9
  "deploy:prod": "wp deploy --tier prod"
10
10
  },
11
11
  "devDependencies": {
12
- "@woodsportal/hubspot-kit": "^1.0.4",
12
+ "@woodsportal/hubspot-kit": "^1.0.11",
13
+ "@dnd-kit/core": "^6.3.1",
14
+ "@dnd-kit/sortable": "^10.0.0",
15
+ "@dnd-kit/utilities": "^3.2.2",
13
16
  "@tailwindcss/vite": "^4.0.6",
17
+ "@tanstack/react-router": "^1.121.2",
14
18
  "@tanstack/router-plugin": "^1.121.2",
15
19
  "@vitejs/plugin-react": "^4.3.4",
16
- "babel-plugin-react-compiler": "^1.0.0",
20
+ "babel-plugin-react-compiler": "^1.0.11",
17
21
  "react": "^19.0.0",
18
22
  "react-dom": "^19.0.0",
19
23
  "tailwindcss": "^4.0.6",
@@ -1 +1,44 @@
1
- [{"name":"title","label":"Title","type":"text","default":"WoodsCLI module"}]
1
+ [
2
+ {
3
+ "name": "title",
4
+ "label": "Portal title",
5
+ "type": "text",
6
+ "tab": "CONTENT",
7
+ "default": "My HubSpot Portal"
8
+ },
9
+ {
10
+ "name": "tagline",
11
+ "label": "Tagline",
12
+ "type": "text",
13
+ "tab": "CONTENT",
14
+ "help_text": "Subtitle shown under the hero title in the local dev preview.",
15
+ "default": "Customize this preview with Ctrl+Shift+Z, then replace StarterPage with your portal app."
16
+ },
17
+ {
18
+ "name": "show_feature_cards",
19
+ "label": "Show feature cards",
20
+ "type": "boolean",
21
+ "display": "toggle",
22
+ "tab": "CONTENT",
23
+ "default": true
24
+ },
25
+ {
26
+ "name": "accent_color",
27
+ "label": "Accent color",
28
+ "type": "color",
29
+ "tab": "STYLE",
30
+ "default": { "color": "#0091ae", "opacity": 100 }
31
+ },
32
+ {
33
+ "name": "hero_style",
34
+ "label": "Hero style",
35
+ "type": "choice",
36
+ "tab": "STYLE",
37
+ "choices": [
38
+ ["gradient", "Gradient"],
39
+ ["minimal", "Minimal"],
40
+ ["bold", "Bold"]
41
+ ],
42
+ "default": "gradient"
43
+ }
44
+ ]
@@ -0,0 +1,8 @@
1
+ import { RouterProvider } from '@tanstack/react-router'
2
+
3
+ import { router } from '@/integrations/tanstack-router/router'
4
+
5
+ /** Root app shell — same pattern as woodsportal-client-frontend `App`. */
6
+ export function App() {
7
+ return <RouterProvider router={router} />
8
+ }
@@ -1,3 +1,8 @@
1
1
  @import 'tailwindcss';
2
2
 
3
- @source '../../.wphs/**/*.{js,jsx,ts,tsx}';
3
+ /* Kit dev-module-config — paths relative to this file (src/assets/css/) */
4
+ @source '../../../.wphs/.kit-module-config/**/*.{ts,tsx}';
5
+ @source '../../../.wphs/.kit-bootstrap.tsx';
6
+ @source '../../../.wphs/.kit-bootstrap-greenfield.tsx';
7
+ @source '../../../src/**/*.{ts,tsx}';
8
+ @source '../../../node_modules/@woodsportal/hubspot-kit/src/dev-module-config/**/*.{ts,tsx}';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * CDN application chunk — mounts after vendor.js (HubSpot require_js order).
3
+ */
4
+ import { mountPortal } from '@/portal/mount-portal'
5
+
6
+ declare const __PORTAL_BUILD_ID__: string | undefined
7
+
8
+ declare global {
9
+ interface Window {
10
+ WoodsPortal?: {
11
+ mount: typeof mountPortal
12
+ version: string
13
+ }
14
+ }
15
+ }
16
+
17
+ const buildId =
18
+ typeof __PORTAL_BUILD_ID__ !== 'undefined' ? String(__PORTAL_BUILD_ID__) : 'dev'
19
+
20
+ window.WoodsPortal = {
21
+ mount: mountPortal,
22
+ version: buildId,
23
+ }
24
+
25
+ async function autoMount() {
26
+ if (!document.getElementById('app')) {
27
+ return
28
+ }
29
+ await mountPortal()
30
+ }
31
+
32
+ void autoMount()
@@ -0,0 +1,28 @@
1
+ /**
2
+ * CDN vendor chunk — React + ReactDOM on `window` for the app IIFE externals.
3
+ */
4
+ import React from 'react'
5
+ import * as ReactDOM from 'react-dom'
6
+ import * as ReactDOMClient from 'react-dom/client'
7
+ import * as jsxRuntime from 'react/jsx-runtime'
8
+ import * as jsxDevRuntime from 'react/jsx-dev-runtime'
9
+
10
+ declare global {
11
+ interface Window {
12
+ WoodsPortalVendor?: {
13
+ React: typeof React
14
+ ReactDOM: typeof ReactDOM
15
+ ReactDOMClient: typeof ReactDOMClient
16
+ jsxRuntime: typeof jsxRuntime
17
+ jsxDevRuntime: typeof jsxDevRuntime
18
+ }
19
+ }
20
+ }
21
+
22
+ window.WoodsPortalVendor = {
23
+ React,
24
+ ReactDOM,
25
+ ReactDOMClient,
26
+ jsxRuntime,
27
+ jsxDevRuntime,
28
+ }