handoff-app 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Changelog.md CHANGED
@@ -6,6 +6,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to
7
7
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## \[1.1.2] - 2026-03-26
10
+
11
+ This release improves integration with downstream platforms (for example HubSpot) by letting
12
+ projects register their own Handlebars helpers during preview generation, instead of relying
13
+ only on Handoff’s built-in `field` and `eq` helpers.
14
+
15
+ ### Feature
16
+
17
+ **`hooks.registerHandlebarsHelpers`** — New optional hook in `handoff.config.js`. After Handoff
18
+ registers its default preview helpers, your callback runs with the Handlebars runtime, the
19
+ current `componentId`, the component `properties` (slot metadata), and a flag for inspect-mode
20
+ renders (`injectFieldWrappers`). Use `handlebars.registerHelper` (and block helpers as needed)
21
+ to add helpers that are only available while building component preview HTML—so templates can
22
+ stay platform-specific without forking Handoff.
23
+
24
+ Typical uses: block helpers that wrap regions meant for a host page’s header or footer, or a
25
+ helper that serializes a value as JSON for inline scripts. Those behaviors are **not** shipped
26
+ as core helpers; you define them in config so each project controls naming and semantics.
27
+
28
+ See `docs/api.md` (Hooks → `registerHandlebarsHelpers`) for the full signature and README
29
+ “Configuration hooks” for a quick pointer.
30
+
31
+ ### Example: JSON in templates
32
+
33
+ If you register a `json` helper in `hooks.registerHandlebarsHelpers`, like this
34
+
35
+ ```
36
+ hooks: {
37
+ registerHandlebarsHelpers: ({ handlebars, componentId }) => {
38
+ // Register 'json' as a simple (non-block) helper
39
+ handlebars.registerHelper('json', (value, data) => {
40
+ return new handlebars.SafeString(JSON.stringify(value, null, 2));
41
+ });
42
+ }
43
+ }
44
+ ```
45
+
46
+ Then your templates can do:
47
+
48
+ ```
49
+ <script type="text/javascript">
50
+ window.barChartData = {{{json properties.data}}};
51
+ </script>
52
+ ```
53
+
54
+ with output shaped by your helper implementation (for example serialized JSON for chart data).
55
+
9
56
  ## \[1.1.1] - 2026-03-12
10
57
 
11
58
  This patch release focuses on Windows stabilization and consistency across environments.
@@ -1830,3 +1877,6 @@ package version was set to ^0.1.0 rather than ^0.2.0. This release fixes that.
1830
1877
 
1831
1878
  ```
1832
1879
  ```
1880
+
1881
+ ```
1882
+ ```
package/README.md CHANGED
@@ -16,6 +16,7 @@ transform tokens into dev ready artifacts.
16
16
  * [How does it work](#how-does-it-work)
17
17
  * [Handoff is in Beta!](#handoff-is-in-beta)
18
18
  * [Requirements](#requirements)
19
+ * [Configuration hooks](#configuration-hooks)
19
20
  * [Get Started](#get-started)
20
21
  * [Figma](#figma)
21
22
  * [Create a project](#create-a-project)
@@ -168,6 +169,12 @@ handoff-app start
168
169
 
169
170
  Once that runs, your browser should update with the new colors.
170
171
 
172
+ ## Configuration hooks
173
+
174
+ Pipeline customization is done in `handoff.config.js` under `hooks` (camelCase names: `validateComponent`, `jsBuildConfig`, `registerHandlebarsHelpers`, etc.). For example, `registerHandlebarsHelpers` runs after Handoff registers the built-in `field` and `eq` helpers so you can call `handlebars.registerHelper` for your `.hbs` preview templates.
175
+
176
+ See [docs/api.md](docs/api.md#hooks) for hook arguments and examples.
177
+
171
178
  ## Further Reading
172
179
 
173
180
  * [Configure your project](https://www.handoff.com/docs/customization)
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ class HandoffCliError extends Error {
19
19
  * Show the help message
20
20
  */
21
21
  const showVersion = () => {
22
- return 'Handoff App - 1.1.1';
22
+ return 'Handoff App - 1.1.2';
23
23
  };
24
24
  /**
25
25
  * Define a CLI error
package/dist/index.d.ts CHANGED
@@ -95,6 +95,6 @@ declare class Handoff {
95
95
  private readJsonFile;
96
96
  }
97
97
  export type { ComponentObject as Component } from './transformers/preview/types';
98
- export type { Config } from './types/config';
98
+ export type { Config, RegisterHandlebarsHelpersContext } from './types/config';
99
99
  export { Transformers as CoreTransformers, TransformerUtils as CoreTransformerUtils, Types as CoreTypes } from 'handoff-core';
100
100
  export default Handoff;
@@ -61,10 +61,11 @@ function processComponentInstances(componentData, documentationComponents) {
61
61
  * @param injectFieldWrappers - Whether to inject field wrappers for inspection
62
62
  * @returns Rendered HTML string
63
63
  */
64
- function renderHandlebarsTemplate(template, componentData, previewData, injectFieldWrappers) {
64
+ function renderHandlebarsTemplate(template, componentData, previewData, injectFieldWrappers, handoff) {
65
65
  return __awaiter(this, void 0, void 0, function* () {
66
+ var _a, _b;
66
67
  // Register Handlebars helpers with current injection state
67
- (0, handlebars_2.registerHandlebarsHelpers)({ id: componentData.id, properties: componentData.properties || {} }, injectFieldWrappers);
68
+ (0, handlebars_2.registerHandlebarsHelpers)({ id: componentData.id, properties: componentData.properties || {} }, injectFieldWrappers, (_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.hooks) === null || _b === void 0 ? void 0 : _b.registerHandlebarsHelpers);
68
69
  const context = (0, handlebars_2.createHandlebarsContext)({
69
70
  id: componentData.id,
70
71
  properties: componentData.properties || {},
@@ -147,8 +148,8 @@ function handlebarsPreviewsPlugin(componentData, documentationComponents, handof
147
148
  try {
148
149
  const previewData = componentData.previews[previewKey];
149
150
  // Render both normal and inspect modes
150
- const normalModeHtml = yield renderHandlebarsTemplate(templateContent, componentData, previewData, false);
151
- const inspectModeHtml = yield renderHandlebarsTemplate(templateContent, componentData, previewData, true);
151
+ const normalModeHtml = yield renderHandlebarsTemplate(templateContent, componentData, previewData, false, handoff);
152
+ const inspectModeHtml = yield renderHandlebarsTemplate(templateContent, componentData, previewData, true, handoff);
152
153
  // Emit preview files
153
154
  emitPreviewFiles(componentId, previewKey, normalModeHtml, inspectModeHtml, (file) => this.emitFile(file));
154
155
  generatedPreviews[previewKey] = normalModeHtml;
@@ -1,16 +1,18 @@
1
+ import { RegisterHandlebarsHelpersContext } from '../../types/config';
1
2
  import { SlotMetadata } from '../preview/component';
2
3
  import { HandlebarsContext } from '../types';
3
4
  /**
4
- * Registers common Handlebars helpers
5
+ * Registers common Handlebars helpers, then runs optional `hooks.registerHandlebarsHelpers`.
5
6
  * @param data - Component data containing properties
6
7
  * @param injectFieldWrappers - Whether to inject field wrappers for inspection
8
+ * @param extend - Optional callback from config to register additional helpers
7
9
  */
8
10
  export declare const registerHandlebarsHelpers: (data: {
9
11
  properties: {
10
12
  [key: string]: SlotMetadata;
11
13
  };
12
14
  id: string;
13
- }, injectFieldWrappers: boolean) => void;
15
+ }, injectFieldWrappers: boolean, extend?: (context: RegisterHandlebarsHelpersContext) => void) => void;
14
16
  /**
15
17
  * Creates Handlebars template context
16
18
  * @param data - Component data
@@ -7,16 +7,17 @@ exports.createHandlebarsContext = exports.registerHandlebarsHelpers = void 0;
7
7
  const handlebars_1 = __importDefault(require("handlebars"));
8
8
  const logger_1 = require("../../utils/logger");
9
9
  /**
10
- * Registers common Handlebars helpers
10
+ * Registers common Handlebars helpers, then runs optional `hooks.registerHandlebarsHelpers`.
11
11
  * @param data - Component data containing properties
12
12
  * @param injectFieldWrappers - Whether to inject field wrappers for inspection
13
+ * @param extend - Optional callback from config to register additional helpers
13
14
  */
14
- const registerHandlebarsHelpers = (data, injectFieldWrappers) => {
15
+ const registerHandlebarsHelpers = (data, injectFieldWrappers, extend) => {
15
16
  // Field helper for property binding
16
17
  handlebars_1.default.registerHelper('field', function (field, options) {
17
18
  if (injectFieldWrappers) {
18
19
  if (!field) {
19
- // This deebuging isn't helpful in the context of the component library
20
+ // This debugging isn't helpful in the context of the component library
20
21
  // Logger.error(`Missing field declaration for ${data.id}`);
21
22
  return options.fn(this);
22
23
  }
@@ -43,6 +44,19 @@ const registerHandlebarsHelpers = (data, injectFieldWrappers) => {
43
44
  handlebars_1.default.registerHelper('eq', function (a, b) {
44
45
  return a === b;
45
46
  });
47
+ if (extend) {
48
+ try {
49
+ extend({
50
+ handlebars: handlebars_1.default,
51
+ componentId: data.id,
52
+ properties: data.properties,
53
+ injectFieldWrappers,
54
+ });
55
+ }
56
+ catch (err) {
57
+ logger_1.Logger.error(`registerHandlebarsHelpers hook failed for ${data.id}`, err);
58
+ }
59
+ }
46
60
  };
47
61
  exports.registerHandlebarsHelpers = registerHandlebarsHelpers;
48
62
  /**
@@ -54,9 +68,7 @@ exports.registerHandlebarsHelpers = registerHandlebarsHelpers;
54
68
  const createHandlebarsContext = (data, previewData, options) => {
55
69
  var _a;
56
70
  const basePath = (_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : '';
57
- const sharedStylesLink = (options === null || options === void 0 ? void 0 : options.includeSharedStyles)
58
- ? `<link rel="stylesheet" href="${basePath}/api/component/shared.css">`
59
- : '';
71
+ const sharedStylesLink = (options === null || options === void 0 ? void 0 : options.includeSharedStyles) ? `<link rel="stylesheet" href="${basePath}/api/component/shared.css">` : '';
60
72
  return {
61
73
  style: `${sharedStylesLink}<link rel="stylesheet" href="${basePath}/api/component/main.css">` +
62
74
  `<link rel="stylesheet" href="${basePath}/api/component/${data.id}.css">\n` +
@@ -1,4 +1,5 @@
1
1
  import { BuildOptions } from 'esbuild';
2
+ import type Handlebars from 'handlebars';
2
3
  import { Types as HandoffTypes } from 'handoff-core';
3
4
  import { InlineConfig } from 'vite';
4
5
  import { SlotMetadata } from '../transformers/preview/component';
@@ -51,6 +52,15 @@ export interface Breakpoints {
51
52
  name: string;
52
53
  };
53
54
  }
55
+ /** Context passed to the `registerHandlebarsHelpers` config hook after built-in helpers (`field`, `eq`) are registered for a preview render. */
56
+ export type RegisterHandlebarsHelpersContext = {
57
+ handlebars: typeof Handlebars;
58
+ componentId: string;
59
+ properties: {
60
+ [key: string]: SlotMetadata;
61
+ };
62
+ injectFieldWrappers: boolean;
63
+ };
54
64
  export interface NextAppConfig {
55
65
  theme?: string;
56
66
  title: string;
@@ -238,6 +248,21 @@ export interface Config {
238
248
  * ```
239
249
  */
240
250
  htmlBuildConfig?: (config: InlineConfig) => InlineConfig;
251
+ /**
252
+ * Optional hook invoked after Handoff registers built-in Handlebars helpers for
253
+ * component preview HTML. Use `context.handlebars.registerHelper` to add or
254
+ * replace helpers. Called once per preview render (per variation and inspect mode).
255
+ *
256
+ * @param context - Handlebars runtime, component id/properties, and whether
257
+ * inspect field wrappers are enabled for this render.
258
+ * @example
259
+ * ```typescript
260
+ * registerHandlebarsHelpers: ({ handlebars, componentId }) => {
261
+ * handlebars.registerHelper('upperId', () => componentId.toUpperCase());
262
+ * }
263
+ * ```
264
+ */
265
+ registerHandlebarsHelpers?: (context: RegisterHandlebarsHelpersContext) => void;
241
266
  };
242
267
  }
243
268
  export type ClientConfig = Pick<Config, 'app' | 'exportsOutputDirectory' | 'sitesOutputDirectory' | 'assets_zip_links' | 'useVariables'>;
package/docs/api.md CHANGED
@@ -78,70 +78,65 @@ to generate only the integration code.
78
78
 
79
79
  ## Hooks
80
80
 
81
- Hooks allow a typescript or javascript app to interact with the pipeline.
82
- This is especially useful for modifying or extending the output of the pipeline.
83
- To use a hook in your project, once you've instantiated the handoff object, you
84
- can call one of the hook methods and pass a function to the method. The hook
85
- function will be called at the appropriate point in the pipeline.
86
-
87
- For example if you want to alter the integration output, you can do this -
81
+ Hooks let you extend the build and preview pipeline. Configure them in
82
+ `handoff.config.js` (or `.cjs`) under the top-level `hooks` object. Hook names
83
+ use camelCase (for example `validateComponent`, `jsBuildConfig`,
84
+ `registerHandlebarsHelpers`). The Handoff CLI and Node API load this config when
85
+ the `Handoff` instance is created.
88
86
 
89
87
  ```js
90
- import Handoff from 'handoff-app';
88
+ // handoff.config.js
89
+ module.exports = {
90
+ // …
91
+ hooks: {
92
+ registerHandlebarsHelpers: ({ handlebars, componentId }) => {
93
+ handlebars.registerHelper('upperId', () => componentId.toUpperCase());
94
+ },
95
+ },
96
+ };
97
+ ```
91
98
 
92
- const handoff = new Handoff({
93
- // You can customize the configuration here
94
- });
99
+ ### registerHandlebarsHelpers
95
100
 
96
- handoff.postIntegration((documentationObject: DocumentationObject, data: HookReturn[]) => {
97
- const colors = documentationObject.design.color.map((color) => {
98
- return {
99
- name: color.name,
100
- value: color.value,
101
- };
102
- });
103
- data.push({
104
- filename: 'colors.json',
105
- data: JSON.stringify(colors, null, 2),
106
- });
107
- return data;
108
- });
101
+ Called immediately after Handoff registers built-in Handlebars helpers (`field`
102
+ and `eq`) while rendering **Handlebars** component previews (static HTML per
103
+ variation). Use it to call `handlebars.registerHelper` for custom helpers used
104
+ in your `.hbs` templates.
109
105
 
110
- handoff.fetch();
111
- ```
106
+ **Arguments (single context object)**
112
107
 
113
- ### postIntegration
108
+ * `handlebars` — The Handlebars runtime (same as importing `handlebars`); use
109
+ `registerHelper`, `SafeString`, etc.
110
+ * `componentId` — The current component’s id string.
111
+ * `properties` — The component’s slot metadata map from the schema (same shape
112
+ as used by the `field` helper).
113
+ * `injectFieldWrappers` — `true` when generating inspect-mode previews (field
114
+ inspection markup), `false` for normal previews.
114
115
 
115
- The Post integration hook will allow you to add a function that will be executed
116
- after the integration build is complete. The function accepts two arguments -
116
+ **Return**
117
117
 
118
- **arguments**
118
+ * `void` — register helpers on `handlebars` directly.
119
119
 
120
- - `tokens: DocumentationObject` This is an instance of the exported
121
- DocumentationObject containing all of the tokens.
122
- - `data: HookReturn[]` This an array of HookReturns, consisting of `filename`
123
- and `data`. Each object in this array will be written out to
124
- `exported/{integration}-tokens`
120
+ **Notes**
125
121
 
126
- **return**
127
- The hook must return an array of `HookReturn` objects, or an empty array
122
+ * This hook runs once per preview render (each variation × normal vs inspect).
123
+ * Registering the same helper name again replaces any previous registration for
124
+ subsequent renders in the same process.
128
125
 
129
- #### Example
126
+ **Example**
130
127
 
131
128
  ```js
132
- handoff.postIntegration((documentationObject: DocumentationObject, data: HookReturn[]) => {
133
- const colors = documentationObject.design.color.map((color) => {
134
- return {
135
- name: color.name,
136
- value: color.value,
137
- };
138
- });
139
- data.push({
140
- filename: 'colors.json',
141
- data: JSON.stringify(colors, null, 2),
142
- });
143
- return data;
144
- });
129
+ // handoff.config.js
130
+ module.exports = {
131
+ hooks: {
132
+ registerHandlebarsHelpers: ({ handlebars, componentId, injectFieldWrappers }) => {
133
+ handlebars.registerHelper('debugPreview', function () {
134
+ return injectFieldWrappers ? '[inspect]' : '[preview]';
135
+ });
136
+ handlebars.registerHelper('componentLabel', () => componentId);
137
+ },
138
+ },
139
+ };
145
140
  ```
146
141
 
147
142
  ### postBuild
@@ -150,7 +145,7 @@ This function is called after the app build is complete.
150
145
 
151
146
  **arguments**
152
147
 
153
- - `tokens: DocumentationObject` This is an instance of the exported
148
+ * `tokens: DocumentationObject` This is an instance of the exported
154
149
  DocumentationObject containing all of the tokens.
155
150
 
156
151
  **returns**
@@ -163,14 +158,14 @@ application to alter the css generation in transit
163
158
 
164
159
  **arguments**
165
160
 
166
- - `tokens: DocumentationObject` This is an instance of the exported
161
+ * `tokens: DocumentationObject` This is an instance of the exported
167
162
  DocumentationObject containing all of the tokens.
168
- - `css: CssTransformerOutput` This is an object containing all of the
163
+ * `css: CssTransformerOutput` This is an object containing all of the
169
164
  tokens for components and foundations, formatted as CSS variables
170
165
 
171
166
  **returns**
172
167
 
173
- - `css: CssTransformerOutput` This is an object containing all of the
168
+ * `css: CssTransformerOutput` This is an object containing all of the
174
169
  tokens for components and foundations, formatted as CSS variables.
175
170
  Any changes you return here will be written to the css files
176
171
 
@@ -182,14 +177,14 @@ the transformed output prior to being written to disk.
182
177
 
183
178
  **arguments**
184
179
 
185
- - `tokens: DocumentationObject` This is an instance of the exported
180
+ * `tokens: DocumentationObject` This is an instance of the exported
186
181
  DocumentationObject containing all of the tokens.
187
- - `css: CssTransformerOutput` This is an object containing all of the
182
+ * `css: CssTransformerOutput` This is an object containing all of the
188
183
  tokens for components and foundations, formatted as CSS variables
189
184
 
190
185
  **returns**
191
186
 
192
- - `css: CssTransformerOutput` This is an object containing all of the
187
+ * `css: CssTransformerOutput` This is an object containing all of the
193
188
  tokens for components and foundations, formatted as CSS variables.
194
189
  Any changes you return here will be written to the css files.
195
190
 
@@ -201,14 +196,14 @@ the transformed output prior to being written to disk.
201
196
 
202
197
  **arguments**
203
198
 
204
- - `tokens: DocumentationObject` This is an instance of the exported
199
+ * `tokens: DocumentationObject` This is an instance of the exported
205
200
  DocumentationObject containing all of the tokens.
206
- - `scss: CssTransformerOutput` This is an object containing all of the
201
+ * `scss: CssTransformerOutput` This is an object containing all of the
207
202
  tokens for components and foundations, formatted as SCSS variables
208
203
 
209
204
  **returns**
210
205
 
211
- - `css: CssTransformerOutput` This is an object containing all of the
206
+ * `css: CssTransformerOutput` This is an object containing all of the
212
207
  tokens for components and foundations, formatted as SCSS variables.
213
208
  Any changes you return here will be written to the scss files.
214
209
 
@@ -224,14 +219,14 @@ the transformed output prior to being written to disk.
224
219
 
225
220
  **arguments**
226
221
 
227
- - `tokens: DocumentationObject` This is an instance of the exported
222
+ * `tokens: DocumentationObject` This is an instance of the exported
228
223
  DocumentationObject containing all of the tokens.
229
- - `scss: CssTransformerOutput` This is an object containing all of the
224
+ * `scss: CssTransformerOutput` This is an object containing all of the
230
225
  type arrays for components and foundations, formatted as SCSS variables
231
226
 
232
227
  **returns**
233
228
 
234
- - `css: CssTransformerOutput` This is an object containing all of the
229
+ * `css: CssTransformerOutput` This is an object containing all of the
235
230
  tokens for components and foundations, formatted as type variables.
236
231
  Any changes you return here will be written to the scss type files.
237
232
 
@@ -244,11 +239,11 @@ and allows you to alter and return that configuration.
244
239
 
245
240
  **arguments**
246
241
 
247
- - `webpackConfig: webpack.Configuration` This is a full webpack configuration.
242
+ * `webpackConfig: webpack.Configuration` This is a full webpack configuration.
248
243
 
249
244
  **returns**
250
245
 
251
- - `webpackConfig: webpack.Configuration` Return the webpack configuration that
246
+ * `webpackConfig: webpack.Configuration` Return the webpack configuration that
252
247
  you have altered to fit your needs.
253
248
 
254
249
  **Example**
@@ -265,15 +260,14 @@ export const modifyWebpackConfigForTailwind = (webpackConfig: webpack.Configurat
265
260
 
266
261
  This hook allows you to alter the exportable list. You could do this by ejecting
267
262
  the handoff configuration and modifying the list, but this hook allows you to
268
- alter the exportable list with just a couple of lines of code rather than
263
+ alter the exportable list with just a couple of lines of code rather than
269
264
  exporting the whole list
270
265
 
271
266
  **arguments**
272
267
 
273
- - `exportables: string[]` The current list of exportables
268
+ * `exportables: string[]` The current list of exportables
274
269
 
275
270
  **returns**
276
271
 
277
- - `exportables: string[]` Return the list with whatever additions and subtractions
278
- you need for your application.
279
-
272
+ * `exportables: string[]` Return the list with whatever additions and subtractions
273
+ you need for your application.
@@ -0,0 +1,90 @@
1
+ /** @type {import('handoff-app').Config} */
2
+ const path = require('path');
3
+ module.exports = {
4
+ // Set your Figma project ID here or in the environment variable HANDOFF_FIGMA_PROJECT_ID
5
+ figma_project_id: null,
6
+ // Set the output directory for the exported components
7
+ exportsOutputDirectory: 'exported',
8
+ // Set the output directory for the sites
9
+ sitesOutputDirectory: 'out',
10
+ // These settings control the behavior of the application
11
+ app: {
12
+ // Set the theme for the application
13
+ theme: 'default',
14
+ // Set the title for the application
15
+ title: 'Handoff Design System',
16
+ // Set the client for the application, used in the footer
17
+ client: 'Handoff Technologies',
18
+ // Set the Google Tag Manager ID to add GTM scripts
19
+ google_tag_manager: null,
20
+ // Set the attribution for the application
21
+ attribution: true, // Should we remove this?
22
+ // Set the type copy for the typeography page
23
+ type_copy: 'Almost before we knew it, we had left the ground.',
24
+ // Set the type sort for the typeography page
25
+ type_sort: [
26
+ 'Heading 1',
27
+ 'Heading 2',
28
+ 'Heading 3',
29
+ 'Heading 4',
30
+ 'Heading 5',
31
+ 'Heading 6',
32
+ 'Paragraph',
33
+ 'Subheading',
34
+ 'Blockquote',
35
+ 'Input Labels',
36
+ 'Link',
37
+ ],
38
+ // Set the color sort for the color page
39
+ color_sort: ['primary', 'secondary', 'extra', 'system'],
40
+ // Set the component sort for the component page
41
+ component_sort: ['primary', 'secondary', 'transparent'],
42
+ // Set the base path for the application
43
+ base_path: '',
44
+ // Set the breakpoints for the application
45
+ breakpoints: {
46
+ sm: { size: 576, name: 'Small' },
47
+ md: { size: 768, name: 'Medium' },
48
+ lg: { size: 992, name: 'Large' },
49
+ xl: { size: 1200, name: 'Extra Large' },
50
+ },
51
+ // Set the ports for the application
52
+ ports: { app: 3000, websocket: 3001 },
53
+ },
54
+ // These are the entry points used to compile the target application
55
+ entries: {
56
+ scss: './sass/main.scss',
57
+ js: './js/main.js',
58
+ bundle: './js/main.js',
59
+ components: ['./test_components'],
60
+ patterns: ['./test_patterns'],
61
+ },
62
+ // These are the asset zip file download links
63
+ assets_zip_links: {
64
+ icons: null,
65
+ logos: null,
66
+ },
67
+ // Hooks allow you to override the default behavior of the application
68
+ hooks: {
69
+ // Alter the vite css build config
70
+ cssBuildConfig: (config) => {
71
+ return config;
72
+ },
73
+ // Validate each component before it is exported
74
+ validateComponent: (component) => {
75
+ return {};
76
+ },
77
+ // Alter the vite client build config
78
+ clientBuildConfig: (config) => {
79
+ return config;
80
+ },
81
+ // Alter the vite html build config
82
+ htmlBuildConfig: (config) => {
83
+ return config;
84
+ },
85
+ // Alter the vite js build config
86
+ jsBuildConfig: (config) => {
87
+ return config;
88
+ },
89
+ },
90
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handoff-app",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Automated documentation toolchain for building client side documentation from figma",
5
5
  "author ": {
6
6
  "name": "Convertiv",
package/src/cli.ts CHANGED
@@ -19,7 +19,7 @@ class HandoffCliError extends Error {
19
19
  * Show the help message
20
20
  */
21
21
  const showVersion = () => {
22
- return 'Handoff App - 1.1.1';
22
+ return 'Handoff App - 1.1.2';
23
23
  };
24
24
 
25
25
  /**
package/src/index.ts CHANGED
@@ -297,7 +297,7 @@ class Handoff {
297
297
  }
298
298
 
299
299
  export type { ComponentObject as Component } from './transformers/preview/types';
300
- export type { Config } from './types/config';
300
+ export type { Config, RegisterHandlebarsHelpersContext } from './types/config';
301
301
 
302
302
  // Export transformers and types from handoff-core
303
303
  export { Transformers as CoreTransformers, TransformerUtils as CoreTransformerUtils, Types as CoreTypes } from 'handoff-core';
@@ -69,12 +69,14 @@ async function renderHandlebarsTemplate(
69
69
  template: string,
70
70
  componentData: TransformComponentTokensResult,
71
71
  previewData: PreviewRenderData,
72
- injectFieldWrappers: boolean
72
+ injectFieldWrappers: boolean,
73
+ handoff: Handoff
73
74
  ): Promise<string> {
74
75
  // Register Handlebars helpers with current injection state
75
76
  registerHandlebarsHelpers(
76
- { id: componentData.id, properties: componentData.properties || {} },
77
- injectFieldWrappers
77
+ { id: componentData.id, properties: componentData.properties || {} },
78
+ injectFieldWrappers,
79
+ handoff.config?.hooks?.registerHandlebarsHelpers
78
80
  );
79
81
 
80
82
  const context = createHandlebarsContext({
@@ -181,14 +183,16 @@ export function handlebarsPreviewsPlugin(
181
183
  templateContent,
182
184
  componentData,
183
185
  previewData,
184
- false
186
+ false,
187
+ handoff
185
188
  );
186
189
 
187
190
  const inspectModeHtml = await renderHandlebarsTemplate(
188
191
  templateContent,
189
192
  componentData,
190
193
  previewData,
191
- true
194
+ true,
195
+ handoff
192
196
  );
193
197
 
194
198
  // Emit preview files
@@ -1,40 +1,43 @@
1
1
  import Handlebars from 'handlebars';
2
+ import { RegisterHandlebarsHelpersContext } from '../../types/config';
2
3
  import { Logger } from '../../utils/logger';
3
4
  import { SlotMetadata } from '../preview/component';
4
5
  import { HandlebarsContext } from '../types';
5
6
 
6
7
  /**
7
- * Registers common Handlebars helpers
8
+ * Registers common Handlebars helpers, then runs optional `hooks.registerHandlebarsHelpers`.
8
9
  * @param data - Component data containing properties
9
10
  * @param injectFieldWrappers - Whether to inject field wrappers for inspection
11
+ * @param extend - Optional callback from config to register additional helpers
10
12
  */
11
13
  export const registerHandlebarsHelpers = (
12
14
  data: { properties: { [key: string]: SlotMetadata }; id: string },
13
- injectFieldWrappers: boolean
15
+ injectFieldWrappers: boolean,
16
+ extend?: (context: RegisterHandlebarsHelpersContext) => void
14
17
  ): void => {
15
18
  // Field helper for property binding
16
19
  Handlebars.registerHelper('field', function (field: string, options: any) {
17
20
  if (injectFieldWrappers) {
18
21
  if (!field) {
19
- // This deebuging isn't helpful in the context of the component library
22
+ // This debugging isn't helpful in the context of the component library
20
23
  // Logger.error(`Missing field declaration for ${data.id}`);
21
24
  return options.fn(this);
22
25
  }
23
-
26
+
24
27
  let parts = field.split('.');
25
28
  let current: any = data.properties;
26
-
29
+
27
30
  for (const part of parts) {
28
31
  if (current?.type === 'object') current = current.properties;
29
32
  else if (current?.type === 'array') current = current.items.properties;
30
33
  current = current?.[part];
31
34
  }
32
-
35
+
33
36
  if (!current) {
34
37
  Logger.error(`Undefined field path for ${data.id}`);
35
38
  return options.fn(this);
36
39
  }
37
-
40
+
38
41
  return new Handlebars.SafeString(
39
42
  `<span class="handoff-field handoff-field-${current?.type || 'unknown'}" data-handoff-field="${field}" data-handoff="${encodeURIComponent(JSON.stringify(current))}">${options.fn(this)}</span>`
40
43
  );
@@ -47,6 +50,19 @@ export const registerHandlebarsHelpers = (
47
50
  Handlebars.registerHelper('eq', function (a: any, b: any) {
48
51
  return a === b;
49
52
  });
53
+
54
+ if (extend) {
55
+ try {
56
+ extend({
57
+ handlebars: Handlebars,
58
+ componentId: data.id,
59
+ properties: data.properties,
60
+ injectFieldWrappers,
61
+ });
62
+ } catch (err) {
63
+ Logger.error(`registerHandlebarsHelpers hook failed for ${data.id}`, err);
64
+ }
65
+ }
50
66
  };
51
67
 
52
68
  /**
@@ -61,9 +77,7 @@ export const createHandlebarsContext = (
61
77
  options?: { includeSharedStyles?: boolean }
62
78
  ): HandlebarsContext => {
63
79
  const basePath = process.env.HANDOFF_APP_BASE_PATH ?? '';
64
- const sharedStylesLink = options?.includeSharedStyles
65
- ? `<link rel="stylesheet" href="${basePath}/api/component/shared.css">`
66
- : '';
80
+ const sharedStylesLink = options?.includeSharedStyles ? `<link rel="stylesheet" href="${basePath}/api/component/shared.css">` : '';
67
81
 
68
82
  return {
69
83
  style:
@@ -1,4 +1,5 @@
1
1
  import { BuildOptions } from 'esbuild';
2
+ import type Handlebars from 'handlebars';
2
3
  import { Types as HandoffTypes } from 'handoff-core';
3
4
  import { InlineConfig } from 'vite';
4
5
  import { SlotMetadata } from '../transformers/preview/component';
@@ -47,6 +48,14 @@ export interface Breakpoints {
47
48
  desktop: { size: number; name: string };
48
49
  }
49
50
 
51
+ /** Context passed to the `registerHandlebarsHelpers` config hook after built-in helpers (`field`, `eq`) are registered for a preview render. */
52
+ export type RegisterHandlebarsHelpersContext = {
53
+ handlebars: typeof Handlebars;
54
+ componentId: string;
55
+ properties: { [key: string]: SlotMetadata };
56
+ injectFieldWrappers: boolean;
57
+ };
58
+
50
59
  export interface NextAppConfig {
51
60
  theme?: string;
52
61
  title: string;
@@ -240,6 +249,22 @@ export interface Config {
240
249
  * ```
241
250
  */
242
251
  htmlBuildConfig?: (config: InlineConfig) => InlineConfig;
252
+
253
+ /**
254
+ * Optional hook invoked after Handoff registers built-in Handlebars helpers for
255
+ * component preview HTML. Use `context.handlebars.registerHelper` to add or
256
+ * replace helpers. Called once per preview render (per variation and inspect mode).
257
+ *
258
+ * @param context - Handlebars runtime, component id/properties, and whether
259
+ * inspect field wrappers are enabled for this render.
260
+ * @example
261
+ * ```typescript
262
+ * registerHandlebarsHelpers: ({ handlebars, componentId }) => {
263
+ * handlebars.registerHelper('upperId', () => componentId.toUpperCase());
264
+ * }
265
+ * ```
266
+ */
267
+ registerHandlebarsHelpers?: (context: RegisterHandlebarsHelpersContext) => void;
243
268
  };
244
269
  }
245
270
 
@@ -183,5 +183,21 @@ module.exports = {
183
183
  // // Add your custom HTML build configuration here
184
184
  // return config;
185
185
  // },
186
+
187
+ /**
188
+ * Optional hook run after built-in Handlebars helpers (`field`, `eq`) for preview HTML.
189
+ * @param context.handlebars - Handlebars module (use registerHelper)
190
+ * @param context.componentId - Current component id
191
+ * @param context.properties - Slot metadata map from the schema
192
+ * @param context.injectFieldWrappers - True when generating inspect-mode previews
193
+ *
194
+ * @example
195
+ * registerHandlebarsHelpers: ({ handlebars, componentId }) => {
196
+ * handlebars.registerHelper('upperId', () => componentId.toUpperCase());
197
+ * }
198
+ */
199
+ // registerHandlebarsHelpers: ({ handlebars, componentId, properties, injectFieldWrappers }) => {
200
+ // // Add your custom Handlebars helpers here
201
+ // },
186
202
  },
187
203
  };
@@ -184,5 +184,21 @@ module.exports = {
184
184
  // // Add your custom HTML build configuration here
185
185
  // return config;
186
186
  // },
187
+
188
+ /**
189
+ * Optional hook run after built-in Handlebars helpers (`field`, `eq`) for preview HTML.
190
+ * @param context.handlebars - Handlebars module (use registerHelper)
191
+ * @param context.componentId - Current component id
192
+ * @param context.properties - Slot metadata map from the schema
193
+ * @param context.injectFieldWrappers - True when generating inspect-mode previews
194
+ *
195
+ * @example
196
+ * registerHandlebarsHelpers: ({ handlebars, componentId }) => {
197
+ * handlebars.registerHelper('upperId', () => componentId.toUpperCase());
198
+ * }
199
+ */
200
+ // registerHandlebarsHelpers: ({ handlebars, componentId, properties, injectFieldWrappers }) => {
201
+ // // Add your custom Handlebars helpers here
202
+ // },
187
203
  },
188
204
  };