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 +50 -0
- package/README.md +7 -0
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/transformers/plugins/handlebars-previews.js +5 -4
- package/dist/transformers/utils/handlebars.d.ts +4 -2
- package/dist/transformers/utils/handlebars.js +18 -6
- package/dist/types/config.d.ts +25 -0
- package/docs/api.md +65 -71
- package/handoff.config.js +90 -0
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/index.ts +1 -1
- package/src/transformers/plugins/handlebars-previews.ts +9 -5
- package/src/transformers/utils/handlebars.ts +24 -10
- package/src/types/config.ts +25 -0
- package/templates/default/handoff.config.js.tpl +16 -0
- package/templates/default/handoff.config.with-examples.js.tpl +16 -0
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
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
|
|
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` +
|
package/dist/types/config.d.ts
CHANGED
|
@@ -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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
// You can customize the configuration here
|
|
94
|
-
});
|
|
99
|
+
### registerHandlebarsHelpers
|
|
95
100
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
111
|
-
```
|
|
106
|
+
**Arguments (single context object)**
|
|
112
107
|
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
after the integration build is complete. The function accepts two arguments -
|
|
116
|
+
**Return**
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
* `void` — register helpers on `handlebars` directly.
|
|
119
119
|
|
|
120
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
126
|
+
**Example**
|
|
130
127
|
|
|
131
128
|
```js
|
|
132
|
-
handoff.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
+
* `tokens: DocumentationObject` This is an instance of the exported
|
|
167
162
|
DocumentationObject containing all of the tokens.
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
+
* `tokens: DocumentationObject` This is an instance of the exported
|
|
186
181
|
DocumentationObject containing all of the tokens.
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
+
* `tokens: DocumentationObject` This is an instance of the exported
|
|
205
200
|
DocumentationObject containing all of the tokens.
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
+
* `tokens: DocumentationObject` This is an instance of the exported
|
|
228
223
|
DocumentationObject containing all of the tokens.
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
+
* `webpackConfig: webpack.Configuration` This is a full webpack configuration.
|
|
248
243
|
|
|
249
244
|
**returns**
|
|
250
245
|
|
|
251
|
-
|
|
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
|
-
|
|
268
|
+
* `exportables: string[]` The current list of exportables
|
|
274
269
|
|
|
275
270
|
**returns**
|
|
276
271
|
|
|
277
|
-
|
|
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
package/src/cli.ts
CHANGED
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
|
|
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:
|
package/src/types/config.ts
CHANGED
|
@@ -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
|
};
|