handoff-app 1.1.0 → 1.1.2-0
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 +58 -0
- package/README.md +7 -0
- package/dist/app-builder/build.js +20 -11
- package/dist/app-builder/watchers.js +8 -9
- package/dist/cache/build-cache.js +25 -10
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/transformers/plugins/handlebars-previews.js +17 -5
- package/dist/transformers/preview/component/builder.js +16 -1
- package/dist/transformers/utils/handlebars.d.ts +4 -2
- package/dist/transformers/utils/handlebars.js +10 -5
- package/dist/transformers/utils/html.js +15 -3
- package/dist/types/config.d.ts +25 -0
- package/dist/utils/path.d.ts +15 -0
- package/dist/utils/path.js +31 -0
- package/docs/api.md +65 -71
- package/package.json +1 -1
- package/src/app/components/Component/ComponentLists.tsx +5 -4
- package/src/app/components/Navigation/MainNav.tsx +3 -5
- package/src/app/components/Navigation/SideNav.tsx +4 -7
- package/src/app/components/SideNav/Custom.tsx +5 -3
- package/src/app/components/ui/mobile-nav.tsx +10 -7
- package/src/app/components/util/index.ts +2 -2
- package/src/app/lib/utils.ts +13 -0
- package/src/app/next.config.mjs +21 -13
- package/src/app/pages/system/component/[component]/index.tsx +1 -1
- package/src/app-builder/build.ts +21 -11
- package/src/app-builder/watchers.ts +9 -8
- package/src/cache/build-cache.ts +29 -11
- package/src/cli.ts +1 -1
- package/src/index.ts +1 -1
- package/src/transformers/plugins/handlebars-previews.ts +22 -6
- package/src/transformers/preview/component/builder.ts +17 -1
- package/src/transformers/utils/handlebars.ts +17 -9
- package/src/transformers/utils/html.ts +16 -3
- package/src/types/config.ts +25 -0
- package/src/utils/path.ts +29 -0
- package/templates/default/handoff.config.js.tpl +16 -0
- package/templates/default/handoff.config.with-examples.js.tpl +16 -0
package/src/cache/build-cache.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import Handoff from '../index';
|
|
4
4
|
import { Logger } from '../utils/logger';
|
|
5
|
+
import { normalizePathForCompare } from '../utils/path';
|
|
5
6
|
import { computeDirectoryState, computeFileState, directoryStatesMatch, FileState, statesMatch } from './file-state';
|
|
6
7
|
|
|
7
8
|
/** Current cache format version - bump when structure changes */
|
|
@@ -162,25 +163,18 @@ export function getComponentFilePaths(handoff: Handoff, componentId: string): {
|
|
|
162
163
|
|
|
163
164
|
const files: string[] = [];
|
|
164
165
|
let templateDir: string | undefined;
|
|
166
|
+
const componentDirs = new Set<string>();
|
|
165
167
|
|
|
166
|
-
//
|
|
167
|
-
const configPaths = handoff.getConfigFilePaths();
|
|
168
|
-
for (const configPath of configPaths) {
|
|
169
|
-
// Check if this config path belongs to this component
|
|
170
|
-
if (configPath.includes(componentId)) {
|
|
171
|
-
files.push(configPath);
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Add entry files
|
|
168
|
+
// Add entry files and infer component directories from resolved entry paths
|
|
177
169
|
const entries = runtimeComponent.entries as Record<string, string | undefined> | undefined;
|
|
178
170
|
if (entries) {
|
|
179
171
|
if (entries.js) {
|
|
180
172
|
files.push(entries.js);
|
|
173
|
+
componentDirs.add(normalizePathForCompare(path.dirname(entries.js)));
|
|
181
174
|
}
|
|
182
175
|
if (entries.scss) {
|
|
183
176
|
files.push(entries.scss);
|
|
177
|
+
componentDirs.add(normalizePathForCompare(path.dirname(entries.scss)));
|
|
184
178
|
}
|
|
185
179
|
// Handle both 'template' (singular) and 'templates' (plural) entry types
|
|
186
180
|
const templatePath = entries.template || entries.templates;
|
|
@@ -189,16 +183,40 @@ export function getComponentFilePaths(handoff: Handoff, componentId: string): {
|
|
|
189
183
|
const stat = fs.statSync(templatePath);
|
|
190
184
|
if (stat.isDirectory()) {
|
|
191
185
|
templateDir = templatePath;
|
|
186
|
+
componentDirs.add(normalizePathForCompare(templatePath));
|
|
192
187
|
} else {
|
|
193
188
|
files.push(templatePath);
|
|
189
|
+
componentDirs.add(normalizePathForCompare(path.dirname(templatePath)));
|
|
194
190
|
}
|
|
195
191
|
} catch {
|
|
196
192
|
// File doesn't exist, still add to track
|
|
197
193
|
files.push(templatePath);
|
|
194
|
+
componentDirs.add(normalizePathForCompare(path.dirname(templatePath)));
|
|
198
195
|
}
|
|
199
196
|
}
|
|
200
197
|
}
|
|
201
198
|
|
|
199
|
+
// Find the config file path for this component using exact config filename + directory matching.
|
|
200
|
+
const configPaths = handoff.getConfigFilePaths();
|
|
201
|
+
const expectedConfigFileNames = new Set([`${componentId}.json`, `${componentId}.js`, `${componentId}.cjs`]);
|
|
202
|
+
const matchingConfigPath = configPaths.find((configPath) => {
|
|
203
|
+
const configFileName = path.basename(configPath);
|
|
204
|
+
if (!expectedConfigFileNames.has(configFileName)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (componentDirs.size === 0) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const configDir = normalizePathForCompare(path.dirname(configPath));
|
|
213
|
+
return componentDirs.has(configDir);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (matchingConfigPath) {
|
|
217
|
+
files.push(matchingConfigPath);
|
|
218
|
+
}
|
|
219
|
+
|
|
202
220
|
return { files, templateDir };
|
|
203
221
|
}
|
|
204
222
|
|
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({
|
|
@@ -158,6 +160,17 @@ export function handlebarsPreviewsPlugin(
|
|
|
158
160
|
// Process component instances from documentation
|
|
159
161
|
processComponentInstances(componentData, documentationComponents);
|
|
160
162
|
|
|
163
|
+
if (!componentData.previews || Object.keys(componentData.previews).length === 0) {
|
|
164
|
+
Logger.warn(`No previews defined for ${componentId}; using default preview values.`);
|
|
165
|
+
componentData.previews = {
|
|
166
|
+
default: {
|
|
167
|
+
title: 'Default',
|
|
168
|
+
values: {},
|
|
169
|
+
url: '',
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
161
174
|
const generatedPreviews: { [key: string]: string } = {};
|
|
162
175
|
|
|
163
176
|
// Generate previews for each variation
|
|
@@ -170,14 +183,16 @@ export function handlebarsPreviewsPlugin(
|
|
|
170
183
|
templateContent,
|
|
171
184
|
componentData,
|
|
172
185
|
previewData,
|
|
173
|
-
false
|
|
186
|
+
false,
|
|
187
|
+
handoff
|
|
174
188
|
);
|
|
175
189
|
|
|
176
190
|
const inspectModeHtml = await renderHandlebarsTemplate(
|
|
177
191
|
templateContent,
|
|
178
192
|
componentData,
|
|
179
193
|
previewData,
|
|
180
|
-
true
|
|
194
|
+
true,
|
|
195
|
+
handoff
|
|
181
196
|
);
|
|
182
197
|
|
|
183
198
|
// Emit preview files
|
|
@@ -202,7 +217,8 @@ export function handlebarsPreviewsPlugin(
|
|
|
202
217
|
componentData.format = PLUGIN_CONSTANTS.OUTPUT_FORMAT;
|
|
203
218
|
componentData.preview = '';
|
|
204
219
|
componentData.code = trimPreview(templateContent);
|
|
205
|
-
|
|
220
|
+
const firstGeneratedPreview = Object.values(generatedPreviews)[0];
|
|
221
|
+
componentData.html = trimPreview(firstGeneratedPreview ?? templateContent);
|
|
206
222
|
},
|
|
207
223
|
};
|
|
208
224
|
}
|
|
@@ -62,6 +62,19 @@ type ComponentBuildPlan = {
|
|
|
62
62
|
validationMode: boolean;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
const ensureDefaultPreview = (data: TransformComponentTokensResult): void => {
|
|
66
|
+
if (!data) return;
|
|
67
|
+
if (!data.previews || Object.keys(data.previews).length === 0) {
|
|
68
|
+
data.previews = {
|
|
69
|
+
default: {
|
|
70
|
+
title: 'Default',
|
|
71
|
+
values: {},
|
|
72
|
+
url: '',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
65
78
|
/**
|
|
66
79
|
* Returns a normalized build plan describing which component segments need rebuilding.
|
|
67
80
|
*
|
|
@@ -223,7 +236,7 @@ export async function processComponents(
|
|
|
223
236
|
// those will override this default (standard "config overrides defaults" behavior).
|
|
224
237
|
if (!restMetadata.figmaComponentId) {
|
|
225
238
|
componentDefaults.previews = {
|
|
226
|
-
|
|
239
|
+
default: {
|
|
227
240
|
title: 'Default',
|
|
228
241
|
values: {},
|
|
229
242
|
url: '',
|
|
@@ -276,6 +289,9 @@ export async function processComponents(
|
|
|
276
289
|
}
|
|
277
290
|
}
|
|
278
291
|
|
|
292
|
+
// Components should always have at least one preview variation.
|
|
293
|
+
ensureDefaultPreview(data);
|
|
294
|
+
|
|
279
295
|
// Build JS if needed (new build, validation missing, or explicit segment request).
|
|
280
296
|
if (buildPlan.js) {
|
|
281
297
|
data = await buildComponentJs(data, handoff);
|
|
@@ -1,16 +1,19 @@
|
|
|
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) {
|
|
@@ -20,21 +23,21 @@ export const registerHandlebarsHelpers = (
|
|
|
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,13 @@ export const registerHandlebarsHelpers = (
|
|
|
47
50
|
Handlebars.registerHelper('eq', function (a: any, b: any) {
|
|
48
51
|
return a === b;
|
|
49
52
|
});
|
|
53
|
+
|
|
54
|
+
extend?.({
|
|
55
|
+
handlebars: Handlebars,
|
|
56
|
+
componentId: data.id,
|
|
57
|
+
properties: data.properties,
|
|
58
|
+
injectFieldWrappers,
|
|
59
|
+
});
|
|
50
60
|
};
|
|
51
61
|
|
|
52
62
|
/**
|
|
@@ -61,9 +71,7 @@ export const createHandlebarsContext = (
|
|
|
61
71
|
options?: { includeSharedStyles?: boolean }
|
|
62
72
|
): HandlebarsContext => {
|
|
63
73
|
const basePath = process.env.HANDOFF_APP_BASE_PATH ?? '';
|
|
64
|
-
const sharedStylesLink = options?.includeSharedStyles
|
|
65
|
-
? `<link rel="stylesheet" href="${basePath}/api/component/shared.css">`
|
|
66
|
-
: '';
|
|
74
|
+
const sharedStylesLink = options?.includeSharedStyles ? `<link rel="stylesheet" href="${basePath}/api/component/shared.css">` : '';
|
|
67
75
|
|
|
68
76
|
return {
|
|
69
77
|
style:
|
|
@@ -7,9 +7,22 @@ import prettier from 'prettier';
|
|
|
7
7
|
* @returns Trimmed HTML content
|
|
8
8
|
*/
|
|
9
9
|
export const trimPreview = (preview: string): string => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
if (typeof preview !== 'string') {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const normalizedPreview = preview.trim();
|
|
15
|
+
if (!normalizedPreview) {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const bodyEl = parse(normalizedPreview).querySelector('body');
|
|
21
|
+
return bodyEl ? bodyEl.innerHTML.trim() : normalizedPreview;
|
|
22
|
+
} catch {
|
|
23
|
+
// Keep build resilient if parser fails on malformed HTML.
|
|
24
|
+
return normalizedPreview;
|
|
25
|
+
}
|
|
13
26
|
};
|
|
14
27
|
|
|
15
28
|
/**
|
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
|
|
package/src/utils/path.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Generates a filesystem-safe directory name from an absolute path
|
|
3
5
|
* @param workingPath - The absolute working path to transform
|
|
@@ -30,3 +32,30 @@ export function generateFilesystemSafeId(workingPath: string): string {
|
|
|
30
32
|
return safeId;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Normalize a filesystem path for cross-platform string comparison.
|
|
37
|
+
* - Resolves to absolute path
|
|
38
|
+
* - Normalizes separators to forward slashes
|
|
39
|
+
* - Lowercases on Windows where paths are case-insensitive
|
|
40
|
+
*/
|
|
41
|
+
export function normalizePathForCompare(inputPath: string): string {
|
|
42
|
+
const normalized = path.resolve(inputPath).replace(/\\/g, '/');
|
|
43
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* True when both paths resolve to the same normalized filesystem path.
|
|
48
|
+
*/
|
|
49
|
+
export function arePathsEqual(pathA: string, pathB: string): boolean {
|
|
50
|
+
return normalizePathForCompare(pathA) === normalizePathForCompare(pathB);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* True when childPath is equal to or within parentPath.
|
|
55
|
+
*/
|
|
56
|
+
export function isPathInside(childPath: string, parentPath: string): boolean {
|
|
57
|
+
const normalizedChild = normalizePathForCompare(childPath);
|
|
58
|
+
const normalizedParent = normalizePathForCompare(parentPath);
|
|
59
|
+
return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}/`);
|
|
60
|
+
}
|
|
61
|
+
|
|
@@ -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
|
};
|