@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,89 @@
1
+ type PersistenceSource = 'local' | 'example' | 'empty'
2
+
3
+ type ModuleConfigPersistenceStatusProps = {
4
+ isDirty: boolean
5
+ persistenceSource: PersistenceSource
6
+ }
7
+
8
+ function StatusIcon({ variant }: { variant: 'dirty' | 'saved' | 'info' }) {
9
+ if (variant === 'dirty') {
10
+ return (
11
+ <svg viewBox="0 0 16 16" className="mc-status-icon" fill="currentColor" aria-hidden>
12
+ <circle cx="8" cy="8" r="3.5" />
13
+ </svg>
14
+ )
15
+ }
16
+
17
+ if (variant === 'saved') {
18
+ return (
19
+ <svg viewBox="0 0 16 16" className="mc-status-icon" fill="none" aria-hidden>
20
+ <path
21
+ d="M3.5 8.5 6.5 11.5 12.5 4.5"
22
+ stroke="currentColor"
23
+ strokeWidth="1.75"
24
+ strokeLinecap="round"
25
+ strokeLinejoin="round"
26
+ />
27
+ </svg>
28
+ )
29
+ }
30
+
31
+ return (
32
+ <svg viewBox="0 0 16 16" className="mc-status-icon" fill="none" aria-hidden>
33
+ <circle cx="8" cy="8" r="6.25" stroke="currentColor" strokeWidth="1.5" />
34
+ <path d="M8 7v4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
35
+ <circle cx="8" cy="4.75" r="0.85" fill="currentColor" />
36
+ </svg>
37
+ )
38
+ }
39
+
40
+ export function ModuleConfigPersistenceStatus({
41
+ isDirty,
42
+ persistenceSource,
43
+ }: ModuleConfigPersistenceStatusProps) {
44
+ if (isDirty) {
45
+ return (
46
+ <div className="mc-persistence-status mc-persistence-status--dirty" role="status">
47
+ <StatusIcon variant="dirty" />
48
+ <div className="mc-persistence-status__body">
49
+ <span className="mc-persistence-status__label">Unsaved changes</span>
50
+ <span className="mc-persistence-status__hint">Apply to update your local config file</span>
51
+ </div>
52
+ </div>
53
+ )
54
+ }
55
+
56
+ if (persistenceSource === 'local') {
57
+ return (
58
+ <div className="mc-persistence-status mc-persistence-status--saved" role="status">
59
+ <StatusIcon variant="saved" />
60
+ <div className="mc-persistence-status__body">
61
+ <span className="mc-persistence-status__label">Saved locally</span>
62
+ <code className="mc-persistence-status__path">.wphs/config/module-config.local.json</code>
63
+ </div>
64
+ </div>
65
+ )
66
+ }
67
+
68
+ if (persistenceSource === 'example') {
69
+ return (
70
+ <div className="mc-persistence-status mc-persistence-status--info" role="status">
71
+ <StatusIcon variant="info" />
72
+ <div className="mc-persistence-status__body">
73
+ <span className="mc-persistence-status__label">Using example config</span>
74
+ <code className="mc-persistence-status__path">.wphs/config/module-config.example.json</code>
75
+ </div>
76
+ </div>
77
+ )
78
+ }
79
+
80
+ return (
81
+ <div className="mc-persistence-status mc-persistence-status--info" role="status">
82
+ <StatusIcon variant="info" />
83
+ <div className="mc-persistence-status__body">
84
+ <span className="mc-persistence-status__label">Default module values</span>
85
+ <span className="mc-persistence-status__hint">From fields.json — apply once to create a local file</span>
86
+ </div>
87
+ </div>
88
+ )
89
+ }
@@ -0,0 +1,355 @@
1
+ import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
2
+ import './module-config-motion.css'
3
+ import './module-config-theme.css'
4
+ import { HUBSPOT_ONLY_FIELD_NAMES, MODULE_CONFIG_CONTENT_TAB_FIELD_NAMES, MODULE_CONFIG_UI_HIDDEN_FIELD_NAMES } from '../schema/types'
5
+ import { hsTypography } from './hubspot/module-config-typography'
6
+ import { flattenVisibleFields } from '../schema/visibility'
7
+ import {
8
+ exportModuleConfig,
9
+ importModuleConfig,
10
+ resetModuleConfigToDefaults,
11
+ } from '../runtime/module-config-runtime'
12
+ import { FieldRenderer } from './FieldRenderer'
13
+ import { MenuOptionsEditor } from './editors/MenuOptionsEditor'
14
+ import { DeveloperOptionsEditor } from './editors/DeveloperOptionsEditor'
15
+ import { useModuleConfig } from './use-module-config'
16
+ import {
17
+ ModuleConfigNavProvider,
18
+ useModuleConfigNav,
19
+ } from './navigation/ModuleConfigNavContext'
20
+ import { ModuleConfigUiProvider, useModuleConfigUi, ModuleConfigUiContext } from './ModuleConfigUiContext'
21
+ import type { ModuleConfigOverlaySide } from './module-config-overlay-side'
22
+ import { ModuleConfigAnimatedPresence } from './ModuleConfigAnimatedPresence'
23
+ import { enterFromClass, mcMotion, type ModuleConfigEnterFrom } from './module-config-motion'
24
+ import { mcSurface } from './module-config-surfaces'
25
+ import { ModuleConfigThemeToggle } from './ModuleConfigThemeToggle'
26
+ import { ModuleConfigDockSideIcon } from './ModuleConfigDockSideIcon'
27
+ import { ModuleConfigPersistenceStatus } from './ModuleConfigPersistenceStatus'
28
+ import { devModuleDisplayName } from '../dev-module-display-name'
29
+
30
+ type Tab = 'CONTENT' | 'STYLE'
31
+
32
+ type ModuleConfigShellProps = {
33
+ compact?: boolean
34
+ showToolbar?: boolean
35
+ onRequestClose?: () => void
36
+ overlaySide?: ModuleConfigOverlaySide
37
+ overlaySideTransitioning?: boolean
38
+ onToggleOverlaySide?: () => void
39
+ }
40
+
41
+ function ModuleConfigShellInner({
42
+ compact = false,
43
+ showToolbar = true,
44
+ onRequestClose,
45
+ overlaySide = 'right',
46
+ overlaySideTransitioning = false,
47
+ onToggleOverlaySide,
48
+ }: ModuleConfigShellProps) {
49
+ const {
50
+ state,
51
+ moduleValues,
52
+ isDirty,
53
+ isApplying,
54
+ validationErrors,
55
+ persistenceSource,
56
+ setFieldValue,
57
+ applyChanges,
58
+ discardDraft,
59
+ confirmDiscardIfDirty,
60
+ } = useModuleConfig()
61
+ const { segments, drillDownPanel } = useModuleConfigNav()
62
+ const { theme, toggleTheme } = useModuleConfigUi()
63
+ const [tab, setTab] = useState<Tab>('CONTENT')
64
+ const [tabEnterFrom, setTabEnterFrom] = useState<ModuleConfigEnterFrom>('right')
65
+
66
+ useEffect(() => {
67
+ if (!onRequestClose) return
68
+ const onKeyDown = (event: KeyboardEvent) => {
69
+ if (event.key !== 'Escape') return
70
+ event.preventDefault()
71
+ if (confirmDiscardIfDirty()) onRequestClose()
72
+ }
73
+ window.addEventListener('keydown', onKeyDown)
74
+ return () => window.removeEventListener('keydown', onKeyDown)
75
+ }, [confirmDiscardIfDirty, onRequestClose])
76
+
77
+ const handleFieldChange = useCallback(
78
+ (path: string, value: unknown) => {
79
+ if (path === 'developerMode' && !value) {
80
+ setFieldValue('developerMode', false)
81
+ setFieldValue('developer_option', false)
82
+ return
83
+ }
84
+ setFieldValue(path, value)
85
+ },
86
+ [setFieldValue],
87
+ )
88
+
89
+ const fields = useMemo(() => {
90
+ if (!state) return []
91
+ const developerModeOn = Boolean(moduleValues.developerMode)
92
+
93
+ const visible = flattenVisibleFields(state.schema, moduleValues, tab)
94
+ .filter(
95
+ (field) =>
96
+ !HUBSPOT_ONLY_FIELD_NAMES.has(field.name) &&
97
+ !MODULE_CONFIG_UI_HIDDEN_FIELD_NAMES.has(field.name),
98
+ )
99
+ .filter(
100
+ (field) => tab === 'CONTENT' || !MODULE_CONFIG_CONTENT_TAB_FIELD_NAMES.has(field.name),
101
+ )
102
+ .filter(
103
+ (field) => field.name !== 'developer_options' || developerModeOn,
104
+ )
105
+
106
+ const developerOptionsField = state.schema.find((field) => field.name === 'developer_options')
107
+ if (
108
+ tab === 'CONTENT' &&
109
+ developerModeOn &&
110
+ developerOptionsField &&
111
+ !visible.some((field) => field.name === 'developer_options')
112
+ ) {
113
+ const developerModeIndex = visible.findIndex((field) => field.name === 'developerMode')
114
+ if (developerModeIndex >= 0) {
115
+ visible.splice(developerModeIndex + 1, 0, developerOptionsField)
116
+ } else {
117
+ visible.push(developerOptionsField)
118
+ }
119
+ }
120
+
121
+ return visible
122
+ }, [state, moduleValues, tab])
123
+
124
+ const developerOptionToggleField = state?.schema.find((field) => field.name === 'developer_option')
125
+
126
+ if (!state) {
127
+ return <div className={`p-4 ${hsTypography.helper}`}>Loading module config…</div>
128
+ }
129
+
130
+ const onExport = () => {
131
+ void exportModuleConfig().then((json) => {
132
+ const blob = new Blob([json], { type: 'application/json' })
133
+ const url = URL.createObjectURL(blob)
134
+ const anchor = document.createElement('a')
135
+ anchor.href = url
136
+ anchor.download = 'woodsportal-module-config.json'
137
+ anchor.click()
138
+ URL.revokeObjectURL(url)
139
+ })
140
+ }
141
+
142
+ const onImport = () => {
143
+ const input = document.createElement('input')
144
+ input.type = 'file'
145
+ input.accept = 'application/json'
146
+ input.onchange = () => {
147
+ const file = input.files?.[0]
148
+ if (!file) return
149
+ void file.text().then(importModuleConfig)
150
+ }
151
+ input.click()
152
+ }
153
+
154
+ const onReset = () => {
155
+ if (!confirmDiscardIfDirty()) return
156
+ void resetModuleConfigToDefaults()
157
+ }
158
+
159
+ const onApply = () => {
160
+ void applyChanges()
161
+ }
162
+
163
+ return (
164
+ <div className={`flex h-full min-h-0 flex-col ${mcSurface.shell}`}>
165
+ <div className={mcSurface.chromeHeader}>
166
+ <nav className={`mb-1 flex flex-wrap items-center gap-1 ${hsTypography.breadcrumb} ${mcSurface.textMuted}`}>
167
+ {segments.map((segment, index) => (
168
+ <span key={segment.id} className="inline-flex items-center gap-1">
169
+ {index > 0 ? <span className={mcSurface.textFaint}>›</span> : null}
170
+ {segment.onClick && segment.id !== 'ellipsis' ? (
171
+ <button type="button" className="hover:underline" onClick={segment.onClick}>
172
+ {segment.label}
173
+ </button>
174
+ ) : (
175
+ <span className={segment.id === 'ellipsis' ? mcSurface.textFaint : mcSurface.textMuted}>
176
+ {segment.label}
177
+ </span>
178
+ )}
179
+ </span>
180
+ ))}
181
+ </nav>
182
+ <div className="flex items-start justify-between gap-2">
183
+ <h2 className={hsTypography.moduleTitle}>{devModuleDisplayName()}</h2>
184
+ <div className="flex shrink-0 items-center gap-1">
185
+ <ModuleConfigThemeToggle theme={theme} onToggle={toggleTheme} />
186
+ {compact && onToggleOverlaySide ? (
187
+ <button
188
+ type="button"
189
+ disabled={overlaySideTransitioning}
190
+ className={mcSurface.iconButton}
191
+ title={overlaySide === 'right' ? 'Dock panel to the left' : 'Dock panel to the right'}
192
+ aria-label={overlaySide === 'right' ? 'Dock panel left' : 'Dock panel right'}
193
+ onClick={onToggleOverlaySide}
194
+ >
195
+ <ModuleConfigDockSideIcon side={overlaySide === 'right' ? 'left' : 'right'} />
196
+ </button>
197
+ ) : null}
198
+ {onRequestClose ? (
199
+ <button
200
+ type="button"
201
+ className={mcSurface.iconButton}
202
+ aria-label="Close module config"
203
+ onClick={() => {
204
+ if (confirmDiscardIfDirty()) onRequestClose()
205
+ }}
206
+ >
207
+ ✕
208
+ </button>
209
+ ) : null}
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <div className={mcSurface.tabBar} role="tablist" aria-label="Module config sections">
215
+ {(['CONTENT', 'STYLE'] as Array<Tab>).map((item) => {
216
+ const isActive = tab === item
217
+ return (
218
+ <button
219
+ key={item}
220
+ type="button"
221
+ role="tab"
222
+ aria-selected={isActive}
223
+ className={`${mcMotion.colors} ${isActive ? mcSurface.tabButtonActive : mcSurface.tabButton}`}
224
+ onClick={() => {
225
+ if (!confirmDiscardIfDirty()) return
226
+ if (isDirty) discardDraft()
227
+ setTabEnterFrom(item === 'STYLE' ? 'right' : 'left')
228
+ setTab(item)
229
+ }}
230
+ >
231
+ {item === 'CONTENT' ? 'Content' : 'Styles'}
232
+ </button>
233
+ )
234
+ })}
235
+ </div>
236
+
237
+ <ModuleConfigAnimatedPresence show={validationErrors.length > 0} enterFrom="bottom">
238
+ <div
239
+ className={`flex-shrink-0 bg-[var(--mc-error-bg)] px-3 py-2 ${hsTypography.error}`}
240
+ >
241
+ {validationErrors.map((error) => (
242
+ <p key={error.path}>{error.message}</p>
243
+ ))}
244
+ </div>
245
+ </ModuleConfigAnimatedPresence>
246
+
247
+ <div className="relative min-h-0 flex-1 overflow-hidden">
248
+ <div
249
+ key={tab}
250
+ className={`h-full overflow-y-auto px-4 py-3 ${enterFromClass(tabEnterFrom)} ${
251
+ drillDownPanel ? 'hidden' : mcMotion.opacity
252
+ }`}
253
+ aria-hidden={Boolean(drillDownPanel)}
254
+ >
255
+ {fields.map((field) => {
256
+ if (field.name === 'menu_options' && field.type === 'group') {
257
+ return (
258
+ <MenuOptionsEditor
259
+ key={field.id ?? field.name}
260
+ field={field}
261
+ moduleValues={moduleValues}
262
+ onChange={handleFieldChange}
263
+ />
264
+ )
265
+ }
266
+
267
+ if (field.name === 'developer_options' && field.type === 'group') {
268
+ return (
269
+ <DeveloperOptionsEditor
270
+ key={field.id ?? field.name}
271
+ groupField={field}
272
+ sectionLabel={developerOptionToggleField?.label ?? 'Developer Options'}
273
+ moduleValues={moduleValues}
274
+ onChange={handleFieldChange}
275
+ />
276
+ )
277
+ }
278
+
279
+ return (
280
+ <FieldRenderer
281
+ key={field.id ?? field.name}
282
+ field={field}
283
+ moduleValues={moduleValues}
284
+ onChange={handleFieldChange}
285
+ compact={compact}
286
+ validationErrors={validationErrors}
287
+ />
288
+ )
289
+ })}
290
+ </div>
291
+ <ModuleConfigAnimatedPresence
292
+ show={Boolean(drillDownPanel)}
293
+ variant="slide"
294
+ enterFrom="right"
295
+ className="absolute inset-0 z-10 overflow-y-auto bg-[var(--mc-bg-shell)] px-4 py-3"
296
+ >
297
+ {drillDownPanel}
298
+ </ModuleConfigAnimatedPresence>
299
+ </div>
300
+
301
+ <div className={mcSurface.chromeFooter}>
302
+ {showToolbar ? (
303
+ <div className="flex flex-wrap gap-2">
304
+ <button type="button" className={mcSurface.footerButton} onClick={onReset}>
305
+ Reset defaults
306
+ </button>
307
+ <button type="button" className={mcSurface.footerButton} onClick={onExport}>
308
+ Export JSON
309
+ </button>
310
+ <button type="button" className={mcSurface.footerButton} onClick={onImport}>
311
+ Import JSON
312
+ </button>
313
+ </div>
314
+ ) : null}
315
+
316
+ <ModuleConfigPersistenceStatus isDirty={isDirty} persistenceSource={persistenceSource} />
317
+
318
+ <button
319
+ type="button"
320
+ disabled={isApplying}
321
+ className={`${mcSurface.primaryButton} ${hsTypography.footerPrimary}`}
322
+ onClick={onApply}
323
+ >
324
+ {isApplying ? 'Applying…' : 'Apply changes'}
325
+ </button>
326
+ </div>
327
+ </div>
328
+ )
329
+ }
330
+
331
+ export function ModuleConfigShell(props: ModuleConfigShellProps) {
332
+ const parentUi = useContext(ModuleConfigUiContext)
333
+ const colorPickerPlacement =
334
+ props.compact && props.overlaySide
335
+ ? props.overlaySide === 'right'
336
+ ? 'left'
337
+ : 'right'
338
+ : props.compact
339
+ ? 'left'
340
+ : 'right'
341
+
342
+ const content = (
343
+ <ModuleConfigNavProvider>
344
+ <ModuleConfigShellInner {...props} />
345
+ </ModuleConfigNavProvider>
346
+ )
347
+
348
+ if (parentUi) return content
349
+
350
+ return (
351
+ <ModuleConfigUiProvider colorPickerPlacement={colorPickerPlacement}>
352
+ {content}
353
+ </ModuleConfigUiProvider>
354
+ )
355
+ }
@@ -0,0 +1,32 @@
1
+ import type { ModuleConfigToolbarTheme } from './module-config-toolbar-theme'
2
+
3
+ type ModuleConfigThemeToggleProps = {
4
+ theme: ModuleConfigToolbarTheme
5
+ onToggle: () => void
6
+ }
7
+
8
+ export function ModuleConfigThemeToggle({ theme, onToggle }: ModuleConfigThemeToggleProps) {
9
+ const isDark = theme === 'dark'
10
+
11
+ return (
12
+ <button
13
+ type="button"
14
+ className="rounded p-1.5 text-[var(--mc-text-muted)] hover:bg-[var(--mc-bg-hover)] hover:text-[var(--mc-text)]"
15
+ title={isDark ? 'Switch to light theme' : 'Switch to dark theme'}
16
+ aria-label={isDark ? 'Switch dev toolbar to light theme' : 'Switch dev toolbar to dark theme'}
17
+ onClick={onToggle}
18
+ >
19
+ {isDark ? (
20
+ <svg viewBox="0 0 20 20" className="h-4 w-4" fill="currentColor" aria-hidden>
21
+ <path d="M10 3.41269C10.3704 3.41269 10.6879 3.09523 10.6879 2.72486V0.68783C10.6879 0.31746 10.3704 0 10 0C9.62965 0 9.31219 0.31746 9.31219 0.68783V2.72486C9.31219 3.09523 9.62965 3.41269 10 3.41269ZM15.6349 5.34392L17.0899 3.88889C17.3545 3.62434 17.3545 3.20106 17.0899 2.93651C16.8254 2.67196 16.4021 2.67196 16.1376 2.93651L14.6825 4.39154C14.418 4.65609 14.418 5.07937 14.6825 5.34392C14.9206 5.60847 15.3439 5.60847 15.6349 5.34392Z" />
22
+ <path d="M10 13.545C12.6455 13.545 14.4445 11.7459 14.4445 9.10045C14.4445 6.45505 12.6455 4.65601 10 4.65601C7.35452 4.65601 5.55548 6.45505 5.55548 9.10045C5.55548 11.7459 7.35452 13.545 10 13.545Z" />
23
+ </svg>
24
+ ) : (
25
+ <svg viewBox="0 0 20 20" className="h-4 w-4" fill="currentColor" aria-hidden>
26
+ <path d="M10 14.5C12.4853 14.5 14.5 12.4853 14.5 10C14.5 7.51472 12.4853 5.5 10 5.5C7.51472 5.5 5.5 7.51472 5.5 10C5.5 12.4853 7.51472 14.5 10 14.5Z" />
27
+ <path d="M10 2.75C10.4142 2.75 10.75 2.41421 10.75 2V0.75C10.75 0.335786 10.4142 0 10 0C9.58579 0 9.25 0.335786 9.25 0.75V2C9.25 2.41421 9.58579 2.75 10 2.75ZM3.51517 4.51517C3.78956 4.24078 4.21056 4.24078 4.48495 4.51517L5.54583 5.57605C5.82022 5.85044 5.82022 6.27144 5.54583 6.54583C5.27144 6.82022 4.85044 6.82022 4.57605 6.54583L3.51517 5.48495C3.24078 5.21056 3.24078 4.78956 3.51517 4.51517ZM2.75 10C2.75 9.58579 2.41421 9.25 2 9.25H0.75C0.335786 9.25 0 9.58579 0 10C0 10.4142 0.335786 10.75 0.75 10.75H2C2.41421 10.75 2.75 10.4142 2.75 10ZM3.51517 15.4849C3.24078 15.2105 3.24078 14.7895 3.51517 14.5152L4.57605 13.4543C4.85044 13.1799 5.27144 13.1799 5.54583 13.4543C5.82022 13.7287 5.82022 14.1497 5.54583 14.4241L4.48495 15.485C4.21056 15.7594 3.78956 15.7594 3.51517 15.4849ZM10 17.25C10.4142 17.25 10.75 17.5858 10.75 18V19.25C10.75 19.6642 10.4142 20 10 20C9.58579 20 9.25 19.6642 9.25 19.25V18C9.25 17.5858 9.58579 17.25 10 17.25ZM15.4849 15.4849C15.7594 15.7594 15.7594 16.1804 15.4849 16.4548L14.4241 17.5157C14.1497 17.7901 13.7287 17.7901 13.4543 17.5157C13.1799 17.2413 13.1799 16.8203 13.4543 16.5459L15.5152 14.485C15.7895 14.2106 16.2105 14.2106 16.4849 14.485ZM17.25 10C17.25 10.4142 17.5858 10.75 18 10.75H19.25C19.6642 10.75 20 10.4142 20 10C20 9.58579 19.6642 9.25 19.25 9.25H18C17.5858 9.25 17.25 9.58579 17.25 10ZM15.4849 4.51517C15.7594 4.24078 16.1804 4.24078 16.4548 4.51517L17.5157 5.57605C17.7901 5.85044 17.7901 6.27144 17.5157 6.54583C17.2413 6.82022 16.8203 6.82022 16.5459 6.54583L15.485 4.48495C15.2106 4.21056 15.2106 3.78956 15.485 3.51517C15.7594 3.24078 16.1804 3.24078 16.4548 3.51517H15.4849Z" />
28
+ </svg>
29
+ )}
30
+ </button>
31
+ )
32
+ }
@@ -0,0 +1,65 @@
1
+ import { createContext, useCallback, useContext, useState, type ReactNode } from 'react'
2
+ import {
3
+ loadModuleConfigToolbarTheme,
4
+ saveModuleConfigToolbarTheme,
5
+ toggleModuleConfigToolbarTheme,
6
+ type ModuleConfigToolbarTheme,
7
+ } from './module-config-toolbar-theme'
8
+
9
+ export type ColorPickerPlacement = 'left' | 'right'
10
+
11
+ export type { ModuleConfigToolbarTheme } from './module-config-toolbar-theme'
12
+
13
+ type ModuleConfigUiContextValue = {
14
+ colorPickerPlacement: ColorPickerPlacement
15
+ theme: ModuleConfigToolbarTheme
16
+ setTheme: (theme: ModuleConfigToolbarTheme) => void
17
+ toggleTheme: () => void
18
+ }
19
+
20
+ const ModuleConfigUiContext = createContext<ModuleConfigUiContextValue | null>(null)
21
+
22
+ export { ModuleConfigUiContext }
23
+
24
+ export function ModuleConfigUiProvider({
25
+ colorPickerPlacement,
26
+ children,
27
+ layout = 'panel',
28
+ }: {
29
+ colorPickerPlacement: ColorPickerPlacement
30
+ children: ReactNode
31
+ /** `overlay` portals: theme vars only, no shell background block in document flow. */
32
+ layout?: 'panel' | 'overlay'
33
+ }) {
34
+ const [theme, setThemeState] = useState<ModuleConfigToolbarTheme>(() => loadModuleConfigToolbarTheme())
35
+
36
+ const setTheme = useCallback((next: ModuleConfigToolbarTheme) => {
37
+ setThemeState(next)
38
+ saveModuleConfigToolbarTheme(next)
39
+ }, [])
40
+
41
+ const toggleTheme = useCallback(() => {
42
+ setThemeState((current) => toggleModuleConfigToolbarTheme(current))
43
+ }, [])
44
+
45
+ const rootClassName =
46
+ layout === 'overlay'
47
+ ? 'mc-ui-overlay-root'
48
+ : 'mc-toolbar-root flex h-full min-h-0 flex-col'
49
+
50
+ return (
51
+ <ModuleConfigUiContext.Provider value={{ colorPickerPlacement, theme, setTheme, toggleTheme }}>
52
+ <div data-mc-theme={theme} className={rootClassName}>
53
+ {children}
54
+ </div>
55
+ </ModuleConfigUiContext.Provider>
56
+ )
57
+ }
58
+
59
+ export function useModuleConfigUi() {
60
+ const ctx = useContext(ModuleConfigUiContext)
61
+ if (!ctx) {
62
+ throw new Error('useModuleConfigUi must be used within ModuleConfigUiProvider')
63
+ }
64
+ return ctx
65
+ }
@@ -0,0 +1,61 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { hsTypography } from '../hubspot/module-config-typography'
3
+ import { FieldRenderer } from '../FieldRenderer'
4
+ import { ModuleConfigCollapsible } from '../ModuleConfigCollapsible'
5
+ import type { HubSpotFieldDefinition, ModuleFieldValues } from '../../schema/types'
6
+
7
+ type DeveloperOptionsEditorProps = {
8
+ groupField: HubSpotFieldDefinition
9
+ sectionLabel?: string
10
+ moduleValues: ModuleFieldValues
11
+ onChange: (path: string, value: unknown) => void
12
+ }
13
+
14
+ /**
15
+ * HubSpot pairs `developer_option` (boolean) with the `developer_options` group:
16
+ * the native editor shows one collapsible "Developer Options" section, not a separate toggle.
17
+ */
18
+ export function DeveloperOptionsEditor({
19
+ groupField,
20
+ sectionLabel = 'Developer Options',
21
+ moduleValues,
22
+ onChange,
23
+ }: DeveloperOptionsEditorProps) {
24
+ const developerModeOn = Boolean(moduleValues.developerMode)
25
+ const enabled = Boolean(moduleValues.developer_option)
26
+ const [open, setOpen] = useState(enabled)
27
+
28
+ useEffect(() => {
29
+ setOpen(enabled)
30
+ }, [enabled])
31
+
32
+ if (!developerModeOn) {
33
+ return null
34
+ }
35
+
36
+ const toggleOpen = () => {
37
+ const next = !open
38
+ setOpen(next)
39
+ onChange('developer_option', next)
40
+ }
41
+
42
+ return (
43
+ <ModuleConfigCollapsible
44
+ open={open}
45
+ onToggle={toggleOpen}
46
+ label={sectionLabel}
47
+ headerClassName={hsTypography.groupHeader}
48
+ >
49
+ {(groupField.children ?? []).map((child) => (
50
+ <FieldRenderer
51
+ key={child.id ?? child.name}
52
+ field={child}
53
+ moduleValues={moduleValues}
54
+ pathPrefix={groupField.name}
55
+ onChange={onChange}
56
+ visibilityContext={{ moduleValues }}
57
+ />
58
+ ))}
59
+ </ModuleConfigCollapsible>
60
+ )
61
+ }