@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,353 @@
1
+ import { useState, type ReactNode } from 'react'
2
+ import { normalizeFieldChoices } from '../schema/normalize-choices'
3
+ import { isFieldVisible } from '../schema/visibility'
4
+ import { getNestedValue } from '../runtime/module-config-runtime'
5
+ import { HsFieldLabel } from './hubspot/HsFieldLabel'
6
+ import { HsOccurrenceGroupEditor } from './hubspot/HsOccurrenceGroupEditor'
7
+ import { HsToggleRow } from './hubspot/HsToggleRow'
8
+ import { hsTypography } from './hubspot/module-config-typography'
9
+ import { mcMotion } from './module-config-motion'
10
+ import { ModuleConfigCollapsible } from './ModuleConfigCollapsible'
11
+ import { HsImageField } from './hubspot/HsImageField'
12
+ import { HsColorField } from './hubspot/HsColorField'
13
+ import {
14
+ HsLinkField,
15
+ HsNumberField,
16
+ HsRichTextField,
17
+ HsTextField,
18
+ } from './hubspot/HsFieldWidgets'
19
+ import type { VisibilityContext } from '../schema/visibility'
20
+ import type { HubSpotFieldDefinition, ModuleFieldValues } from '../schema/types'
21
+ import { isModuleConfigSectionDefaultCollapsed } from '../schema/types'
22
+
23
+ type FieldRendererProps = {
24
+ field: HubSpotFieldDefinition
25
+ moduleValues: ModuleFieldValues
26
+ pathPrefix?: string
27
+ /** Parent repeater scope for nested drill-down panels (e.g. menu_options.objects.0) */
28
+ editorScope?: string
29
+ onChange: (path: string, value: unknown) => void
30
+ compact?: boolean
31
+ visibilityContext?: VisibilityContext
32
+ validationErrors?: Array<{ path: string; message: string }>
33
+ }
34
+
35
+ function fieldPath(prefix: string | undefined, name: string) {
36
+ return prefix ? `${prefix}.${name}` : name
37
+ }
38
+
39
+ function readRaw(moduleValues: ModuleFieldValues, path: string): unknown {
40
+ return getNestedValue(moduleValues, path)
41
+ }
42
+
43
+ function ColorField({
44
+ field,
45
+ value,
46
+ onChange,
47
+ path,
48
+ }: {
49
+ field: HubSpotFieldDefinition
50
+ value: { color?: string; opacity?: number | string }
51
+ onChange: (path: string, value: unknown) => void
52
+ path: string
53
+ }) {
54
+ return (
55
+ <HsColorField
56
+ label={field.label}
57
+ required={field.required}
58
+ helpText={field.help_text}
59
+ value={value}
60
+ onChange={(next) => onChange(path, next)}
61
+ />
62
+ )
63
+ }
64
+
65
+ function BooleanField({
66
+ field,
67
+ value,
68
+ onChange,
69
+ path,
70
+ }: {
71
+ field: HubSpotFieldDefinition
72
+ value: boolean
73
+ onChange: (path: string, value: unknown) => void
74
+ path: string
75
+ }) {
76
+ const useToggle = field.display === 'toggle' || field.display === undefined || field.display === ''
77
+
78
+ if (useToggle) {
79
+ return (
80
+ <HsToggleRow
81
+ label={field.label}
82
+ required={field.required}
83
+ helpText={field.help_text}
84
+ checked={Boolean(value)}
85
+ onChange={(checked) => onChange(path, checked)}
86
+ />
87
+ )
88
+ }
89
+
90
+ return (
91
+ <label className={`mb-4 flex items-center justify-between gap-3 ${mcMotion.transition}`}>
92
+ <HsFieldLabel label={field.label} required={field.required} helpText={field.help_text} />
93
+ <input
94
+ type="checkbox"
95
+ checked={Boolean(value)}
96
+ onChange={(e) => onChange(path, e.target.checked)}
97
+ className={`h-4 w-4 mc-checkbox ${mcMotion.transition}`}
98
+ />
99
+ </label>
100
+ )
101
+ }
102
+
103
+ function ChoiceField({
104
+ field,
105
+ value,
106
+ onChange,
107
+ path,
108
+ }: {
109
+ field: HubSpotFieldDefinition
110
+ value: string
111
+ onChange: (path: string, value: unknown) => void
112
+ path: string
113
+ }) {
114
+ const choices = normalizeFieldChoices(field)
115
+
116
+ return (
117
+ <div className="mb-4">
118
+ <HsFieldLabel label={field.label} required={field.required} helpText={field.help_text} />
119
+ <select
120
+ className={`w-full rounded px-3 py-2 focus:outline-none ${hsTypography.input} mc-select ${mcMotion.fieldControl}`}
121
+ value={value}
122
+ onChange={(e) => onChange(path, e.target.value)}
123
+ >
124
+ {choices.map((choice) => (
125
+ <option key={choice.value} value={choice.value}>
126
+ {choice.label}
127
+ </option>
128
+ ))}
129
+ </select>
130
+ </div>
131
+ )
132
+ }
133
+
134
+ function groupInitialOpen(field: HubSpotFieldDefinition): boolean {
135
+ if (isModuleConfigSectionDefaultCollapsed(field.name)) {
136
+ return false
137
+ }
138
+ return field.expanded ?? true
139
+ }
140
+
141
+ function GroupField({
142
+ field,
143
+ moduleValues,
144
+ pathPrefix,
145
+ onChange,
146
+ compact,
147
+ visibilityContext,
148
+ validationErrors,
149
+ }: FieldRendererProps) {
150
+ const [open, setOpen] = useState(() => groupInitialOpen(field))
151
+ const path = fieldPath(pathPrefix, field.name)
152
+
153
+ return (
154
+ <ModuleConfigCollapsible
155
+ open={open}
156
+ onToggle={() => setOpen((v) => !v)}
157
+ label={field.label}
158
+ headerClassName={hsTypography.groupHeader}
159
+ >
160
+ {(field.children ?? []).map((child) => (
161
+ <FieldRenderer
162
+ key={child.id ?? child.name}
163
+ field={child}
164
+ moduleValues={moduleValues}
165
+ pathPrefix={path}
166
+ onChange={onChange}
167
+ compact={compact}
168
+ visibilityContext={visibilityContext}
169
+ validationErrors={validationErrors}
170
+ />
171
+ ))}
172
+ </ModuleConfigCollapsible>
173
+ )
174
+ }
175
+
176
+ function isOccurrenceGroup(field: HubSpotFieldDefinition): boolean {
177
+ return Boolean(field.occurrence && field.children?.length)
178
+ }
179
+
180
+ function FieldError({ path, validationErrors }: { path: string; validationErrors?: FieldRendererProps['validationErrors'] }) {
181
+ const error = validationErrors?.find((item) => item.path === path)
182
+ if (!error) return null
183
+ return <p className={`mb-2 ${hsTypography.error}`}>{error.message}</p>
184
+ }
185
+
186
+ export function FieldRenderer({
187
+ field,
188
+ moduleValues,
189
+ pathPrefix,
190
+ editorScope,
191
+ onChange,
192
+ compact,
193
+ visibilityContext,
194
+ validationErrors,
195
+ }: FieldRendererProps) {
196
+ const visibilityCtx = visibilityContext ?? { moduleValues }
197
+ if (!isFieldVisible(field, visibilityCtx)) return null
198
+
199
+ const path = fieldPath(pathPrefix, field.name)
200
+ const raw = readRaw(moduleValues, path)
201
+
202
+ if (field.type === 'group' && !isOccurrenceGroup(field)) {
203
+ return (
204
+ <GroupField
205
+ field={field}
206
+ moduleValues={moduleValues}
207
+ pathPrefix={pathPrefix}
208
+ onChange={onChange}
209
+ compact={compact}
210
+ visibilityContext={visibilityContext}
211
+ validationErrors={validationErrors}
212
+ />
213
+ )
214
+ }
215
+
216
+ if (isOccurrenceGroup(field)) {
217
+ const items = Array.isArray(raw) ? (raw as Array<ModuleFieldValues>) : []
218
+ const rootModule = visibilityContext?.moduleValues ?? moduleValues
219
+ const labelField =
220
+ field.name === 'properties'
221
+ ? 'properties_value'
222
+ : field.name === 'dashboard_cards'
223
+ ? 'card_title'
224
+ : 'list_object_name'
225
+
226
+ return (
227
+ <>
228
+ <FieldError path={path} validationErrors={validationErrors} />
229
+ <HsOccurrenceGroupEditor
230
+ field={field}
231
+ items={items}
232
+ moduleValues={rootModule}
233
+ path={path}
234
+ editorScope={editorScope}
235
+ onChange={onChange}
236
+ labelField={labelField}
237
+ sectionLabel={field.label?.trim() || field.label}
238
+ visibilityContext={visibilityContext}
239
+ />
240
+ </>
241
+ )
242
+ }
243
+
244
+ const wrap = (node: ReactNode) => (
245
+ <div className="mc-field-enter">
246
+ <FieldError path={path} validationErrors={validationErrors} />
247
+ {node}
248
+ </div>
249
+ )
250
+
251
+ if (field.type === 'boolean') {
252
+ return wrap(<BooleanField field={field} value={Boolean(raw)} onChange={onChange} path={path} />)
253
+ }
254
+
255
+ if (field.type === 'color') {
256
+ return wrap(
257
+ <ColorField
258
+ field={field}
259
+ value={(raw as { color?: string; opacity?: number }) ?? { color: '', opacity: 100 }}
260
+ onChange={onChange}
261
+ path={path}
262
+ />,
263
+ )
264
+ }
265
+
266
+ if (field.type === 'image') {
267
+ return wrap(
268
+ <HsImageField
269
+ label={field.label}
270
+ required={field.required}
271
+ helpText={field.help_text}
272
+ value={(raw as Record<string, unknown>) ?? { src: '', alt: '' }}
273
+ onChange={(value) => onChange(path, value)}
274
+ />,
275
+ )
276
+ }
277
+
278
+ if (field.type === 'link' || field.type === 'url') {
279
+ const linkValue =
280
+ raw && typeof raw === 'object' && !Array.isArray(raw)
281
+ ? (raw as {
282
+ url?: { href?: string; type?: string }
283
+ open_in_new_tab?: boolean
284
+ no_follow?: boolean
285
+ })
286
+ : {
287
+ url: { href: typeof raw === 'string' ? raw : '', type: 'EXTERNAL' },
288
+ open_in_new_tab: false,
289
+ no_follow: false,
290
+ }
291
+ return wrap(
292
+ <HsLinkField
293
+ label={field.label}
294
+ required={field.required}
295
+ helpText={field.help_text}
296
+ value={linkValue}
297
+ onChange={(value) => onChange(path, value)}
298
+ />,
299
+ )
300
+ }
301
+
302
+ if (field.type === 'choice') {
303
+ return wrap(<ChoiceField field={field} value={String(raw ?? '')} onChange={onChange} path={path} />)
304
+ }
305
+
306
+ if (field.type === 'richtext') {
307
+ return wrap(
308
+ <HsRichTextField
309
+ label={field.label}
310
+ required={field.required}
311
+ helpText={field.help_text}
312
+ value={String(raw ?? '')}
313
+ onChange={(value) => onChange(path, value)}
314
+ />,
315
+ )
316
+ }
317
+
318
+ if (field.type === 'number') {
319
+ return wrap(
320
+ <HsNumberField
321
+ label={field.label}
322
+ required={field.required}
323
+ helpText={field.help_text}
324
+ value={Number(raw ?? 0)}
325
+ onChange={(value) => onChange(path, value)}
326
+ />,
327
+ )
328
+ }
329
+
330
+ if (field.type === 'text') {
331
+ const isSvg = field.name.includes('svg') || field.label.toLowerCase().includes('svg')
332
+ return wrap(
333
+ <HsTextField
334
+ label={field.label}
335
+ required={field.required}
336
+ helpText={field.help_text}
337
+ value={String(raw ?? '')}
338
+ onChange={(value) => onChange(path, value)}
339
+ multiline={isSvg}
340
+ monospace={isSvg}
341
+ darkEditor={isSvg}
342
+ />,
343
+ )
344
+ }
345
+
346
+ if (compact) return null
347
+
348
+ return (
349
+ <div className={`mb-2 ${hsTypography.helper}`}>
350
+ Unsupported field: {field.label} ({field.type})
351
+ </div>
352
+ )
353
+ }
@@ -0,0 +1,72 @@
1
+ import { useEffect, useRef, useState, type ReactNode } from 'react'
2
+ import { getModuleConfigMotionMs, mcMotion, type ModuleConfigEnterFrom } from './module-config-motion'
3
+
4
+ type ModuleConfigAnimatedPresenceProps = {
5
+ show: boolean
6
+ children: ReactNode
7
+ className?: string
8
+ variant?: 'slide' | 'fade'
9
+ enterFrom?: ModuleConfigEnterFrom
10
+ }
11
+
12
+ export function ModuleConfigAnimatedPresence({
13
+ show,
14
+ children,
15
+ className = '',
16
+ variant = 'fade',
17
+ enterFrom = 'bottom',
18
+ }: ModuleConfigAnimatedPresenceProps) {
19
+ const [mounted, setMounted] = useState(show)
20
+ const [visible, setVisible] = useState(show)
21
+ const contentRef = useRef<ReactNode>(children)
22
+
23
+ if (show || mounted) {
24
+ contentRef.current = children
25
+ }
26
+
27
+ useEffect(() => {
28
+ const motionMs = getModuleConfigMotionMs()
29
+
30
+ if (show) {
31
+ setMounted(true)
32
+ if (motionMs === 0) {
33
+ setVisible(true)
34
+ return
35
+ }
36
+ const frame = requestAnimationFrame(() => {
37
+ requestAnimationFrame(() => setVisible(true))
38
+ })
39
+ return () => cancelAnimationFrame(frame)
40
+ }
41
+
42
+ setVisible(false)
43
+ const timer = window.setTimeout(() => setMounted(false), motionMs)
44
+ return () => clearTimeout(timer)
45
+ }, [show])
46
+
47
+ if (!mounted) return null
48
+
49
+ if (variant === 'slide') {
50
+ const slideFrom = enterFrom === 'left' ? 'left' : 'right'
51
+
52
+ return (
53
+ <div
54
+ className={`mc-slide-panel ${className}`.trim()}
55
+ data-visible={visible ? 'true' : 'false'}
56
+ data-enter-from={slideFrom}
57
+ >
58
+ {contentRef.current}
59
+ </div>
60
+ )
61
+ }
62
+
63
+ return (
64
+ <div
65
+ className={`${mcMotion.transition} ${className} ${
66
+ visible ? 'translate-y-0 opacity-100' : 'pointer-events-none translate-y-2 opacity-0'
67
+ }`.trim()}
68
+ >
69
+ {contentRef.current}
70
+ </div>
71
+ )
72
+ }
@@ -0,0 +1,43 @@
1
+ import type { ReactNode } from 'react'
2
+ import { mcMotion } from './module-config-motion'
3
+ import { mcSurface } from './module-config-surfaces'
4
+
5
+ type ModuleConfigCollapsibleProps = {
6
+ open: boolean
7
+ label: ReactNode
8
+ onToggle: () => void
9
+ children: ReactNode
10
+ headerClassName?: string
11
+ }
12
+
13
+ export function ModuleConfigCollapsible({
14
+ open,
15
+ label,
16
+ onToggle,
17
+ children,
18
+ headerClassName = '',
19
+ }: ModuleConfigCollapsibleProps) {
20
+ return (
21
+ <div className={`mb-3 ${mcSurface.panel}`}>
22
+ <button
23
+ type="button"
24
+ className={`flex w-full items-center justify-between px-3 py-2.5 text-left ${headerClassName}`}
25
+ onClick={onToggle}
26
+ aria-expanded={open}
27
+ >
28
+ {label}
29
+ <span
30
+ className={`inline-block ${mcSurface.textFaint} ${mcMotion.transform} ${open ? 'rotate-0' : '-rotate-90'}`}
31
+ aria-hidden
32
+ >
33
+ ▾
34
+ </span>
35
+ </button>
36
+ <div className={`mc-collapsible-grid ${open ? 'mc-collapsible-open' : ''}`}>
37
+ <div className="mc-collapsible-inner">
38
+ <div className="mc-collapsible-body px-3 py-2">{children}</div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,27 @@
1
+ type DockSide = 'left' | 'right'
2
+
3
+ type ModuleConfigDockSideIconProps = {
4
+ /** Target side when clicked (where the panel will dock). */
5
+ side: DockSide
6
+ className?: string
7
+ }
8
+
9
+ /** Rounded frame with a filled sidebar on left or right — matches dev toolbar dock control. */
10
+ export function ModuleConfigDockSideIcon({ side, className = 'h-4 w-4' }: ModuleConfigDockSideIconProps) {
11
+ const barX = side === 'left' ? 4.5 : 11.5
12
+
13
+ return (
14
+ <svg viewBox="0 0 20 20" className={className} fill="none" aria-hidden>
15
+ <rect
16
+ x="3"
17
+ y="4"
18
+ width="14"
19
+ height="12"
20
+ rx="2"
21
+ stroke="currentColor"
22
+ strokeWidth="1.5"
23
+ />
24
+ <rect x={barX} y="6" width="4" height="8" rx="0.75" fill="currentColor" />
25
+ </svg>
26
+ )
27
+ }
@@ -0,0 +1,134 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import './module-config-theme.css'
4
+ import { ModuleConfigShell } from './ModuleConfigShell'
5
+ import { ModuleConfigUiProvider } from './ModuleConfigUiContext'
6
+ import {
7
+ loadModuleConfigOverlaySide,
8
+ toggleModuleConfigOverlaySide,
9
+ type ModuleConfigOverlaySide,
10
+ } from './module-config-overlay-side'
11
+
12
+ const OVERLAY_TRANSITION_MS = 280
13
+
14
+ function getTransitionMs(): number {
15
+ if (typeof window === 'undefined') return OVERLAY_TRANSITION_MS
16
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : OVERLAY_TRANSITION_MS
17
+ }
18
+
19
+ export function ModuleConfigOverlay() {
20
+ const [open, setOpen] = useState(false)
21
+ const [mounted, setMounted] = useState(false)
22
+ const [entered, setEntered] = useState(false)
23
+ const [side, setSide] = useState<ModuleConfigOverlaySide>(() => loadModuleConfigOverlaySide())
24
+ const [sideTransitioning, setSideTransitioning] = useState(false)
25
+ const sideSwitchTimerRef = useRef<number | null>(null)
26
+
27
+ const clearSideSwitchTimer = useCallback(() => {
28
+ if (sideSwitchTimerRef.current != null) {
29
+ window.clearTimeout(sideSwitchTimerRef.current)
30
+ sideSwitchTimerRef.current = null
31
+ }
32
+ }, [])
33
+
34
+ const requestClose = useCallback(() => {
35
+ setOpen(false)
36
+ }, [])
37
+
38
+ useEffect(() => () => clearSideSwitchTimer(), [clearSideSwitchTimer])
39
+
40
+ useEffect(() => {
41
+ const onKeyDown = (event: KeyboardEvent) => {
42
+ if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key.toLowerCase() === 'z') {
43
+ event.preventDefault()
44
+ setOpen((value) => !value)
45
+ }
46
+ }
47
+ window.addEventListener('keydown', onKeyDown)
48
+ return () => window.removeEventListener('keydown', onKeyDown)
49
+ }, [])
50
+
51
+ useEffect(() => {
52
+ const transitionMs = getTransitionMs()
53
+
54
+ if (open) {
55
+ setMounted(true)
56
+ if (transitionMs === 0) {
57
+ setEntered(true)
58
+ return
59
+ }
60
+ const enterFrame = requestAnimationFrame(() => {
61
+ requestAnimationFrame(() => setEntered(true))
62
+ })
63
+ return () => cancelAnimationFrame(enterFrame)
64
+ }
65
+
66
+ setEntered(false)
67
+ setSideTransitioning(false)
68
+ clearSideSwitchTimer()
69
+ const timer = window.setTimeout(() => setMounted(false), transitionMs)
70
+ return () => clearTimeout(timer)
71
+ }, [clearSideSwitchTimer, open])
72
+
73
+ const onToggleSide = useCallback(() => {
74
+ if (sideTransitioning || !entered) return
75
+
76
+ const transitionMs = getTransitionMs()
77
+ if (transitionMs === 0) {
78
+ setSide((current) => toggleModuleConfigOverlaySide(current))
79
+ return
80
+ }
81
+
82
+ setSideTransitioning(true)
83
+ setEntered(false)
84
+ clearSideSwitchTimer()
85
+
86
+ sideSwitchTimerRef.current = window.setTimeout(() => {
87
+ sideSwitchTimerRef.current = null
88
+ setSide((current) => toggleModuleConfigOverlaySide(current))
89
+ requestAnimationFrame(() => {
90
+ requestAnimationFrame(() => {
91
+ setEntered(true)
92
+ setSideTransitioning(false)
93
+ })
94
+ })
95
+ }, transitionMs)
96
+ }, [clearSideSwitchTimer, entered, sideTransitioning])
97
+
98
+ if (!mounted) return null
99
+
100
+ const colorPickerPlacement = side === 'right' ? 'left' : 'right'
101
+ const enteredClass = entered ? 'mc-overlay-entered' : ''
102
+ const inertClass = entered && !sideTransitioning ? '' : 'mc-overlay-inert'
103
+
104
+ return createPortal(
105
+ <ModuleConfigUiProvider colorPickerPlacement={colorPickerPlacement} layout="overlay">
106
+ <div aria-hidden className={`mc-overlay-backdrop ${enteredClass}`} />
107
+ <aside
108
+ role="dialog"
109
+ aria-modal="true"
110
+ aria-label="Module config"
111
+ data-side={side}
112
+ className={`mc-toolbar-root mc-overlay-drawer ${enteredClass} ${inertClass}`}
113
+ >
114
+ <div className="min-h-0 flex-1">
115
+ <ModuleConfigShell
116
+ compact
117
+ overlaySide={side}
118
+ overlaySideTransitioning={sideTransitioning}
119
+ onToggleOverlaySide={onToggleSide}
120
+ onRequestClose={requestClose}
121
+ showToolbar
122
+ />
123
+ </div>
124
+ </aside>
125
+ </ModuleConfigUiProvider>,
126
+ document.body,
127
+ )
128
+ }
129
+
130
+ export function registerModuleConfigShortcutHint() {
131
+ if (import.meta.env.DEV) {
132
+ console.info('[WoodsPortal] Module config overlay: Ctrl+Shift+Z (dock left/right in panel header)')
133
+ }
134
+ }