handoff-app 1.1.3 → 1.2.0-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.
Files changed (143) hide show
  1. package/config/config.template.js +21 -14
  2. package/config/config.template.ts +54 -0
  3. package/config/docs/system.md +2 -0
  4. package/dist/app-builder/build.js +7 -2
  5. package/dist/app-builder/config-diff/index.d.ts +4 -0
  6. package/dist/app-builder/config-diff/index.js +12 -0
  7. package/dist/app-builder/config-diff/registry.d.ts +9 -0
  8. package/dist/app-builder/config-diff/registry.js +30 -0
  9. package/dist/app-builder/config-diff/snapshot.d.ts +13 -0
  10. package/dist/app-builder/config-diff/snapshot.js +43 -0
  11. package/dist/app-builder/config-diff/strategies/pattern.d.ts +2 -0
  12. package/dist/app-builder/config-diff/strategies/pattern.js +93 -0
  13. package/dist/app-builder/config-diff/types.d.ts +19 -0
  14. package/dist/app-builder/config-diff/types.js +2 -0
  15. package/dist/app-builder/watchers/component.d.ts +15 -0
  16. package/dist/app-builder/watchers/component.js +69 -0
  17. package/dist/app-builder/watchers/utils.d.ts +14 -0
  18. package/dist/app-builder/watchers/utils.js +44 -0
  19. package/dist/app-builder/watchers.d.ts +10 -19
  20. package/dist/app-builder/watchers.js +56 -102
  21. package/dist/cache/build-cache.js +14 -5
  22. package/dist/cli/create.js +67 -23
  23. package/dist/cli/eject.js +48 -7
  24. package/dist/cli/make.js +29 -4
  25. package/dist/cli/scaffold.js +60 -19
  26. package/dist/cli.js +1 -1
  27. package/dist/config/helpers.d.ts +7 -0
  28. package/dist/config/helpers.js +31 -0
  29. package/dist/config/index.d.ts +2 -1
  30. package/dist/config/index.js +5 -1
  31. package/dist/config/loader.d.ts +10 -1
  32. package/dist/config/loader.js +92 -28
  33. package/dist/config/normalizers/declaration.d.ts +11 -0
  34. package/dist/config/normalizers/declaration.js +119 -0
  35. package/dist/config/normalizers/pattern.d.ts +9 -0
  36. package/dist/config/normalizers/pattern.js +38 -0
  37. package/dist/config/runtime.d.ts +9 -3
  38. package/dist/config/runtime.js +254 -33
  39. package/dist/declarations/helpers.d.ts +6 -0
  40. package/dist/declarations/helpers.js +19 -0
  41. package/dist/declarations/index.d.ts +2 -0
  42. package/dist/declarations/index.js +9 -0
  43. package/dist/declarations/types.d.ts +60 -0
  44. package/dist/declarations/types.js +2 -0
  45. package/dist/index.d.ts +12 -1
  46. package/dist/index.js +34 -5
  47. package/dist/pipeline/index.d.ts +1 -0
  48. package/dist/pipeline/index.js +3 -1
  49. package/dist/pipeline/patterns.d.ts +6 -0
  50. package/dist/pipeline/patterns.js +21 -0
  51. package/dist/transformers/docgen/index.js +12 -2
  52. package/dist/transformers/plugins/csf-render.js +170 -34
  53. package/dist/transformers/plugins/ssr-render.js +18 -2
  54. package/dist/transformers/preview/component/api.js +16 -7
  55. package/dist/transformers/preview/component/builder.js +9 -3
  56. package/dist/transformers/preview/component/css.d.ts +8 -0
  57. package/dist/transformers/preview/component/html.js +14 -5
  58. package/dist/transformers/preview/component/previews.d.ts +9 -0
  59. package/dist/transformers/preview/component/previews.js +10 -0
  60. package/dist/transformers/preview/pattern/api.d.ts +7 -0
  61. package/dist/transformers/preview/pattern/api.js +61 -0
  62. package/dist/transformers/preview/pattern/builder.d.ts +9 -0
  63. package/dist/transformers/preview/pattern/builder.js +86 -0
  64. package/dist/transformers/preview/pattern/html.d.ts +19 -0
  65. package/dist/transformers/preview/pattern/html.js +94 -0
  66. package/dist/transformers/preview/types.d.ts +54 -0
  67. package/dist/transformers/utils/csf.d.ts +16 -0
  68. package/dist/transformers/utils/csf.js +30 -0
  69. package/dist/transformers/utils/index.d.ts +1 -0
  70. package/dist/transformers/utils/index.js +2 -0
  71. package/dist/transformers/utils/module.d.ts +5 -0
  72. package/dist/transformers/utils/module.js +26 -13
  73. package/dist/transformers/utils/usage.d.ts +23 -0
  74. package/dist/transformers/utils/usage.js +294 -0
  75. package/dist/types/config.d.ts +32 -7
  76. package/dist/types/preview.d.ts +2 -0
  77. package/docs/cli.md +3 -3
  78. package/package.json +3 -1
  79. package/src/app/components/Component/PageSliceResolver.tsx +6 -6
  80. package/src/app/components/Component/Preview.tsx +17 -12
  81. package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +16 -6
  82. package/src/app/components/Markdown/CodeHighlight.tsx +60 -11
  83. package/src/app/components/util/index.ts +71 -2
  84. package/src/app/pages/system/pattern/[pattern]/index.tsx +223 -0
  85. package/src/app/pages/system/pattern/index.tsx +14 -0
  86. package/src/app-builder/README.md +37 -9
  87. package/src/app-builder/build.ts +8 -2
  88. package/src/app-builder/config-diff/index.ts +4 -0
  89. package/src/app-builder/config-diff/registry.ts +23 -0
  90. package/src/app-builder/config-diff/snapshot.ts +51 -0
  91. package/src/app-builder/config-diff/strategies/pattern.ts +65 -0
  92. package/src/app-builder/config-diff/types.ts +21 -0
  93. package/src/app-builder/watchers/component.ts +72 -0
  94. package/src/app-builder/watchers/utils.ts +44 -0
  95. package/src/app-builder/watchers.ts +62 -80
  96. package/src/cache/build-cache.ts +14 -5
  97. package/src/cli/create.ts +86 -25
  98. package/src/cli/eject.ts +29 -7
  99. package/src/cli/make.ts +32 -5
  100. package/src/cli/scaffold.ts +68 -21
  101. package/src/cli.ts +1 -1
  102. package/src/config/README.md +10 -1
  103. package/src/config/helpers.ts +43 -0
  104. package/src/config/index.ts +4 -1
  105. package/src/config/loader.ts +110 -31
  106. package/src/config/normalizers/declaration.ts +176 -0
  107. package/src/config/normalizers/pattern.ts +63 -0
  108. package/src/config/runtime.ts +310 -35
  109. package/src/declarations/helpers.ts +36 -0
  110. package/src/declarations/index.ts +19 -0
  111. package/src/declarations/types.ts +70 -0
  112. package/src/index.ts +47 -6
  113. package/src/pipeline/index.ts +1 -0
  114. package/src/pipeline/patterns.ts +10 -0
  115. package/src/transformers/README.md +2 -2
  116. package/src/transformers/docgen/index.ts +13 -2
  117. package/src/transformers/plugins/csf-render.ts +216 -49
  118. package/src/transformers/plugins/ssr-render.ts +23 -7
  119. package/src/transformers/preview/component/api.ts +27 -7
  120. package/src/transformers/preview/component/builder.ts +21 -4
  121. package/src/transformers/preview/component/html.ts +18 -5
  122. package/src/transformers/preview/component/previews.ts +12 -0
  123. package/src/transformers/preview/pattern/api.ts +66 -0
  124. package/src/transformers/preview/pattern/builder.ts +94 -0
  125. package/src/transformers/preview/pattern/html.ts +107 -0
  126. package/src/transformers/preview/types.ts +58 -1
  127. package/src/transformers/utils/csf.ts +48 -0
  128. package/src/transformers/utils/index.ts +3 -0
  129. package/src/transformers/utils/module.ts +33 -18
  130. package/src/transformers/utils/usage.ts +319 -0
  131. package/src/types/README.md +1 -1
  132. package/src/types/config.ts +33 -7
  133. package/src/types/preview.ts +2 -0
  134. package/templates/default/components/button/Button.jsx.tpl +64 -0
  135. package/templates/default/components/button/button.react.handoff.js.tpl +31 -0
  136. package/templates/default/components/button/button.react.handoff.ts.tpl +31 -0
  137. package/templates/default/handoff.config.js.tpl +29 -16
  138. package/templates/default/handoff.config.ts.tpl +196 -0
  139. package/templates/default/handoff.config.with-examples.js.tpl +28 -17
  140. package/templates/default/handoff.config.with-examples.ts.tpl +195 -0
  141. package/templates/default/package.json.tpl +1 -3
  142. package/handoff.config.js +0 -90
  143. package/templates/default/components/button/button.js.tpl +0 -62
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extractComponentName = extractComponentName;
7
+ exports.generateUsageSnippet = generateUsageSnippet;
8
+ const path_1 = __importDefault(require("path"));
9
+ const component_1 = require("../preview/component");
10
+ const CHILDREN_PROP = 'children';
11
+ const INDENT = ' ';
12
+ function isValidIdentifier(key) {
13
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key);
14
+ }
15
+ /**
16
+ * Formats a prop value as a JSX attribute string.
17
+ * Returns the right-hand side only (e.g. `"hello"`, `{42}`, `{true}`).
18
+ */
19
+ function formatPropValue(value, type) {
20
+ if (value === undefined || value === null) {
21
+ return '{undefined}';
22
+ }
23
+ switch (type) {
24
+ case component_1.SlotType.BOOLEAN:
25
+ return typeof value === 'boolean' ? `{${value}}` : `{${Boolean(value)}}`;
26
+ case component_1.SlotType.NUMBER:
27
+ return `{${Number(value)}}`;
28
+ case component_1.SlotType.FUNCTION:
29
+ return '{() => { /* handler */ }}';
30
+ case component_1.SlotType.OBJECT:
31
+ return `{${formatObjectLiteral(value)}}`;
32
+ case component_1.SlotType.ARRAY:
33
+ return `{${formatArrayLiteral(value)}}`;
34
+ case component_1.SlotType.TEXT:
35
+ case component_1.SlotType.IMAGE:
36
+ case component_1.SlotType.BUTTON:
37
+ case component_1.SlotType.ENUM:
38
+ default:
39
+ if (typeof value === 'string') {
40
+ return `"${escapeJsxString(value)}"`;
41
+ }
42
+ if (typeof value === 'boolean') {
43
+ return `{${value}}`;
44
+ }
45
+ if (typeof value === 'number') {
46
+ return `{${value}}`;
47
+ }
48
+ if (typeof value === 'object') {
49
+ return `{${JSON.stringify(value, null, 2)}}`;
50
+ }
51
+ return `"${escapeJsxString(String(value))}"`;
52
+ }
53
+ }
54
+ function escapeJsxString(str) {
55
+ return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
56
+ }
57
+ function formatObjectLiteral(value) {
58
+ if (typeof value !== 'object' || value === null) {
59
+ return JSON.stringify(value);
60
+ }
61
+ const entries = Object.entries(value);
62
+ if (entries.length === 0)
63
+ return '{}';
64
+ if (entries.length <= 3) {
65
+ const inline = entries
66
+ .map(([k, v]) => `${isValidIdentifier(k) ? k : JSON.stringify(k)}: ${JSON.stringify(v)}`)
67
+ .join(', ');
68
+ if (inline.length <= 60)
69
+ return `{ ${inline} }`;
70
+ }
71
+ return JSON.stringify(value, null, 2);
72
+ }
73
+ function formatArrayLiteral(value) {
74
+ if (!Array.isArray(value))
75
+ return JSON.stringify(value);
76
+ if (value.length === 0)
77
+ return '[]';
78
+ const inline = JSON.stringify(value);
79
+ if (inline.length <= 60)
80
+ return inline;
81
+ return JSON.stringify(value, null, 2);
82
+ }
83
+ /**
84
+ * Determines whether a prop value matches its declared default and can be omitted.
85
+ */
86
+ function matchesDefault(value, slotDefault) {
87
+ if (slotDefault === undefined || slotDefault === null)
88
+ return false;
89
+ if (typeof value === typeof slotDefault && value === slotDefault)
90
+ return true;
91
+ // SlotMetadata.default is typed as `string | undefined`, but runtime values
92
+ // may be numbers or booleans. Do a loose string comparison as a fallback.
93
+ if (String(value) === String(slotDefault))
94
+ return true;
95
+ return false;
96
+ }
97
+ /**
98
+ * Extracts the PascalCase component name from a file path.
99
+ * e.g. `/path/to/Button.tsx` -> `Button`
100
+ */
101
+ function extractComponentName(filePath) {
102
+ const basename = path_1.default.basename(filePath);
103
+ const ext = path_1.default.extname(basename);
104
+ return basename.slice(0, basename.length - ext.length);
105
+ }
106
+ /**
107
+ * Generates a JSX usage snippet showing how to consume a React component
108
+ * with the given preview values.
109
+ *
110
+ * Iterates over the union of preview values and schema properties so that
111
+ * props not present in the preview are still represented when they carry
112
+ * a meaningful default or are required.
113
+ */
114
+ function generateUsageSnippet(options) {
115
+ var _a;
116
+ const { componentName, properties, previewValues, templateFileName, importStatement } = options;
117
+ const importPath = `./${templateFileName ? path_1.default.basename(templateFileName, path_1.default.extname(templateFileName)) : componentName}`;
118
+ const attrs = [];
119
+ let childrenValue = undefined;
120
+ let hasChildren = false;
121
+ // Merge all known prop keys: preview values first (preserve order), then
122
+ // any additional keys from the property schema that weren't in the preview.
123
+ const allKeys = new Set([...Object.keys(previewValues), ...Object.keys(properties)]);
124
+ for (const key of allKeys) {
125
+ const slot = properties[key];
126
+ const inPreview = key in previewValues;
127
+ const value = inPreview ? previewValues[key] : resolveSchemaValue(slot);
128
+ // Nothing to show for this prop
129
+ if (value === undefined || value === null) {
130
+ continue;
131
+ }
132
+ if (key === CHILDREN_PROP) {
133
+ hasChildren = true;
134
+ childrenValue = value;
135
+ continue;
136
+ }
137
+ // Skip function props that weren't explicitly provided in preview values
138
+ if (!inPreview && (slot === null || slot === void 0 ? void 0 : slot.type) === component_1.SlotType.FUNCTION) {
139
+ continue;
140
+ }
141
+ // Omit props that match their declared default
142
+ if (slot && matchesDefault(value, slot.default)) {
143
+ continue;
144
+ }
145
+ const type = (_a = slot === null || slot === void 0 ? void 0 : slot.type) !== null && _a !== void 0 ? _a : inferSlotType(value);
146
+ // Boolean true → JSX shorthand: <Comp disabled />
147
+ if (type === component_1.SlotType.BOOLEAN && value === true) {
148
+ attrs.push(key);
149
+ continue;
150
+ }
151
+ // Boolean false → omit (implicit JSX default)
152
+ if (type === component_1.SlotType.BOOLEAN && value === false) {
153
+ continue;
154
+ }
155
+ attrs.push(`${key}=${formatPropValue(value, type)}`);
156
+ }
157
+ const lines = [];
158
+ lines.push(importStatement || `import ${componentName} from '${importPath}';`);
159
+ lines.push('');
160
+ if (hasChildren && childrenValue !== undefined && childrenValue !== null && childrenValue !== '') {
161
+ const openTag = attrs.length > 0 ? `<${componentName} ${attrs.join(' ')}>` : `<${componentName}>`;
162
+ const childStr = typeof childrenValue === 'string'
163
+ ? childrenValue
164
+ : typeof childrenValue === 'number' || typeof childrenValue === 'boolean'
165
+ ? String(childrenValue)
166
+ : '{/* children */}';
167
+ if (childStr.length <= 40 && !childStr.includes('\n')) {
168
+ lines.push(`${openTag}${childStr}</${componentName}>`);
169
+ }
170
+ else {
171
+ lines.push(openTag);
172
+ for (const childLine of childStr.split('\n')) {
173
+ lines.push(`${INDENT}${childLine}`);
174
+ }
175
+ lines.push(`</${componentName}>`);
176
+ }
177
+ }
178
+ else {
179
+ if (attrs.length === 0) {
180
+ lines.push(`<${componentName} />`);
181
+ }
182
+ else if (attrs.length <= 3 && attrs.join(' ').length <= 60) {
183
+ lines.push(`<${componentName} ${attrs.join(' ')} />`);
184
+ }
185
+ else {
186
+ lines.push(`<${componentName}`);
187
+ for (const attr of attrs) {
188
+ lines.push(`${INDENT}${attr}`);
189
+ }
190
+ lines.push('/>');
191
+ }
192
+ }
193
+ return lines.join('\n');
194
+ }
195
+ /**
196
+ * Resolves a display value for a schema-only property (not present in preview values).
197
+ *
198
+ * - Uses the declared default when available and non-trivial.
199
+ * - For required props without a default, generates a type-appropriate placeholder.
200
+ * - Returns `undefined` for optional props with no useful default (they are skipped).
201
+ */
202
+ function resolveSchemaValue(slot) {
203
+ var _a;
204
+ if (!slot)
205
+ return undefined;
206
+ // Use explicit default if it's non-trivial
207
+ if (slot.default !== undefined && slot.default !== null && slot.default !== '') {
208
+ return coerceDefault(slot.default, slot.type);
209
+ }
210
+ // Required props must appear even without a default
211
+ if ((_a = slot.rules) === null || _a === void 0 ? void 0 : _a.required) {
212
+ return getPlaceholder(slot);
213
+ }
214
+ // Optional with no default → omit
215
+ return undefined;
216
+ }
217
+ /**
218
+ * Coerces a string default from SlotMetadata into the appropriate JS type
219
+ * for code-generation purposes.
220
+ */
221
+ function coerceDefault(value, type) {
222
+ if (typeof value !== 'string')
223
+ return value;
224
+ switch (type) {
225
+ case component_1.SlotType.BOOLEAN:
226
+ return value === 'true';
227
+ case component_1.SlotType.NUMBER: {
228
+ const n = Number(value);
229
+ return Number.isNaN(n) ? value : n;
230
+ }
231
+ default:
232
+ return value;
233
+ }
234
+ }
235
+ /**
236
+ * Generates a type-appropriate placeholder value for required props
237
+ * that have no default and no preview value.
238
+ */
239
+ function getPlaceholder(slot) {
240
+ switch (slot.type) {
241
+ case component_1.SlotType.ENUM: {
242
+ const member = extractFirstEnumMember(slot);
243
+ return member !== null && member !== void 0 ? member : 'value';
244
+ }
245
+ case component_1.SlotType.TEXT:
246
+ case component_1.SlotType.IMAGE:
247
+ return '...';
248
+ case component_1.SlotType.NUMBER:
249
+ return 0;
250
+ case component_1.SlotType.BOOLEAN:
251
+ return true;
252
+ case component_1.SlotType.ARRAY:
253
+ return [];
254
+ case component_1.SlotType.OBJECT:
255
+ return {};
256
+ case component_1.SlotType.FUNCTION:
257
+ return undefined; // handled separately
258
+ default:
259
+ return '...';
260
+ }
261
+ }
262
+ /**
263
+ * Attempts to extract the first enum member from a SlotMetadata's deepType
264
+ * (e.g. `"primary"` from `'primary' | 'secondary' | 'danger'`).
265
+ */
266
+ function extractFirstEnumMember(slot) {
267
+ var _a, _b;
268
+ // deepType.members contains the enum values when available
269
+ if (((_a = slot.deepType) === null || _a === void 0 ? void 0 : _a.kind) === 'enum' && ((_b = slot.deepType.members) === null || _b === void 0 ? void 0 : _b.length)) {
270
+ const raw = slot.deepType.members[0];
271
+ // Members are stored as quoted strings like `"primary"` — strip quotes
272
+ return typeof raw === 'string' ? raw.replace(/^["']|["']$/g, '') : undefined;
273
+ }
274
+ // Fall back to parsing the generic display string (e.g. `"primary" | "secondary"`)
275
+ if (slot.generic) {
276
+ const match = slot.generic.match(/["']([^"']+)["']/);
277
+ if (match)
278
+ return match[1];
279
+ }
280
+ return undefined;
281
+ }
282
+ function inferSlotType(value) {
283
+ if (typeof value === 'boolean')
284
+ return component_1.SlotType.BOOLEAN;
285
+ if (typeof value === 'number')
286
+ return component_1.SlotType.NUMBER;
287
+ if (typeof value === 'function')
288
+ return component_1.SlotType.FUNCTION;
289
+ if (Array.isArray(value))
290
+ return component_1.SlotType.ARRAY;
291
+ if (typeof value === 'object' && value !== null)
292
+ return component_1.SlotType.OBJECT;
293
+ return component_1.SlotType.TEXT;
294
+ }
@@ -3,7 +3,7 @@ import type Handlebars from 'handlebars';
3
3
  import { Types as HandoffTypes } from 'handoff-core';
4
4
  import { InlineConfig } from 'vite';
5
5
  import { SlotMetadata } from '../transformers/preview/component';
6
- import { ComponentListObject, TransformComponentTokensResult } from '../transformers/preview/types';
6
+ import { ComponentListObject, PatternListObject, TransformComponentTokensResult } from '../transformers/preview/types';
7
7
  import { ValidationResult } from './preview';
8
8
  export interface ImageStyle {
9
9
  name: string;
@@ -65,13 +65,19 @@ export interface NextAppConfig {
65
65
  theme?: string;
66
66
  title: string;
67
67
  client: string;
68
- google_tag_manager: string | null | undefined;
69
- type_copy: string;
70
- type_sort: string[];
71
- color_sort: string[];
68
+ google_tag_manager?: string | null | undefined;
69
+ googleTagManager?: string | null | undefined;
70
+ type_copy?: string;
71
+ typeCopy?: string;
72
+ type_sort?: string[];
73
+ typeSort?: string[];
74
+ color_sort?: string[];
75
+ colorSort?: string[];
72
76
  breakpoints: Breakpoints;
73
- component_sort: string[];
74
- base_path: string;
77
+ component_sort?: string[];
78
+ componentSort?: string[];
79
+ base_path?: string;
80
+ basePath?: string;
75
81
  attribution: boolean;
76
82
  ports?: {
77
83
  app: number;
@@ -80,7 +86,9 @@ export interface NextAppConfig {
80
86
  }
81
87
  export interface Config {
82
88
  dev_access_token?: string | null | undefined;
89
+ devAccessToken?: string | null | undefined;
83
90
  figma_project_id?: string | null | undefined;
91
+ figmaProjectId?: string | null | undefined;
84
92
  exportsOutputDirectory?: string;
85
93
  sitesOutputDirectory?: string;
86
94
  useVariables?: boolean;
@@ -123,6 +131,12 @@ export interface Config {
123
131
  * @example ["components/button", "components/input"]
124
132
  */
125
133
  components?: string[];
134
+ /**
135
+ * Array of pattern paths to be included in the build.
136
+ * Patterns compose multiple component previews into single-page views.
137
+ * @example ["patterns/hero-section", "patterns"]
138
+ */
139
+ patterns?: string[];
126
140
  };
127
141
  /**
128
142
  * Configuration for asset zip file download links
@@ -140,6 +154,10 @@ export interface Config {
140
154
  */
141
155
  logos?: string;
142
156
  };
157
+ assetsZipLinks?: {
158
+ icons?: string;
159
+ logos?: string;
160
+ };
143
161
  /**
144
162
  * Configuration hooks for extending functionality
145
163
  */
@@ -278,6 +296,10 @@ export interface RuntimeConfigComponentOptions {
278
296
  };
279
297
  };
280
298
  }
299
+ export interface ConfigFileEntry {
300
+ kind: string;
301
+ entityId: string;
302
+ }
281
303
  export interface RuntimeConfig {
282
304
  entries?: {
283
305
  scss?: string;
@@ -286,6 +308,9 @@ export interface RuntimeConfig {
286
308
  components: {
287
309
  [id: string]: ComponentListObject;
288
310
  };
311
+ patterns: {
312
+ [id: string]: PatternListObject;
313
+ };
289
314
  };
290
315
  options: {
291
316
  [key: string]: RuntimeConfigComponentOptions;
@@ -54,6 +54,7 @@ export interface PreviewObject {
54
54
  [key: string]: string;
55
55
  };
56
56
  url: string;
57
+ usage?: string;
57
58
  };
58
59
  };
59
60
  properties?: {
@@ -61,6 +62,7 @@ export interface PreviewObject {
61
62
  };
62
63
  code: string;
63
64
  html?: string;
65
+ usage?: string;
64
66
  format: string;
65
67
  variant?: Record<string, string>;
66
68
  options?: {
package/docs/cli.md CHANGED
@@ -14,9 +14,9 @@ The CLI will allow you to build the various kinds of configurations that you
14
14
  will need for interacting with Figma. Handoff has sane configuration defaults
15
15
  but the various kinds of configurations can be tailored or extended.
16
16
 
17
- Handoff has 4 configuration files -
17
+ Handoff has 4 configuration areas -
18
18
 
19
- `handoff.config.json` - Defines the general handoff configuration.
19
+ `handoff.config.*` - Defines the general handoff configuration. Supported files are `handoff.config.ts`, `handoff.config.js`, `handoff.config.cjs`, and `handoff.config.json` (resolved in that precedence order).
20
20
  `pages` - Markdown files that will create or customize pages in the documentation
21
21
  app
22
22
  `exportables` - JSON schemas for each component in your figma file that you
@@ -28,7 +28,7 @@ The CLI exposes two ways to manage the config - `make` and `eject`.
28
28
 
29
29
  __Eject__ commands will take the default configuration and eject them into
30
30
  the current working directory. If you customize these configurations, then run
31
- handoff commands in that directory, these configs will be executed.
31
+ handoff commands in that directory, these configs will be executed. `eject:config` now prompts for TypeScript or JavaScript config format.
32
32
  __Make__ commands will generate a boilerplate configuration in the current
33
33
  working directory. This is useful for extending handoff for different components
34
34
  or integrations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handoff-app",
3
- "version": "1.1.3",
3
+ "version": "1.2.0-0",
4
4
  "description": "Automated documentation toolchain for building client side documentation from figma",
5
5
  "author ": {
6
6
  "name": "Convertiv",
@@ -105,6 +105,8 @@
105
105
  "prettier": "^3.4.2",
106
106
  "react": "^19.1.0",
107
107
  "react-dom": "^19.1.0",
108
+ "react-element-to-jsx-string": "^17.0.1",
109
+ "react-is": "^19.1.0",
108
110
  "react-markdown": "^10.1.0",
109
111
  "react-scroll": "^1.8.9",
110
112
  "react-syntax-highlighter": "^16.1.0",
@@ -8,8 +8,8 @@ export interface PageSliceResolverProps {
8
8
  preview: PreviewObject;
9
9
  title: string;
10
10
  height?: string;
11
- currentValues?: Record<string, string>;
12
- onValuesChange?: (values: Record<string, string>) => void;
11
+ currentPreviewUrl?: string;
12
+ onPreviewChange?: (previewUrl: string) => void;
13
13
  bestPracticesCard?: boolean;
14
14
  codeHighlight?: boolean;
15
15
  properties?: boolean;
@@ -21,8 +21,8 @@ export const PageSliceResolver: React.FC<PageSliceResolverProps> = ({
21
21
  preview,
22
22
  title,
23
23
  height,
24
- currentValues,
25
- onValuesChange,
24
+ currentPreviewUrl,
25
+ onPreviewChange,
26
26
  bestPracticesCard = true,
27
27
  codeHighlight = true,
28
28
  properties = true,
@@ -39,8 +39,8 @@ export const PageSliceResolver: React.FC<PageSliceResolverProps> = ({
39
39
  preview={preview}
40
40
  title={title}
41
41
  height={height}
42
- currentValues={currentValues}
43
- onValuesChange={onValuesChange}
42
+ currentPreviewUrl={currentPreviewUrl}
43
+ onPreviewChange={onPreviewChange}
44
44
  showPreview={slice.showPreview}
45
45
  showCodeHighlight={slice.showCodeHighlight}
46
46
  defaultHeight={slice.defaultHeight}
@@ -55,8 +55,8 @@ export const ComponentDisplay: React.FC<{
55
55
  component: PreviewObject | undefined;
56
56
  defaultHeight?: string | undefined;
57
57
  title?: string;
58
- onValuesChange?: (values: Record<string, string>) => void;
59
- }> = ({ component, defaultHeight, title, onValuesChange }) => {
58
+ onPreviewChange?: (previewUrl: string) => void;
59
+ }> = ({ component, defaultHeight, title, onPreviewChange }) => {
60
60
  const context = usePreviewContext();
61
61
  const ref = React.useRef<HTMLIFrameElement>(null);
62
62
  const [height, setHeight] = React.useState('100px');
@@ -131,11 +131,10 @@ export const ComponentDisplay: React.FC<{
131
131
  if (keys.length === 0) {
132
132
  return;
133
133
  }
134
- // check the environment
135
- setPreviewUrl(component.previews[keys[0]].url);
136
- !!onValuesChange && onValuesChange(component.previews[keys[0]].values);
134
+ const firstPreview = component.previews[keys[0]];
135
+ setPreviewUrl(firstPreview.url);
137
136
  }
138
- }, [component, onValuesChange]);
137
+ }, [component]);
139
138
 
140
139
  React.useEffect(() => {
141
140
  if (!component) return;
@@ -147,11 +146,17 @@ export const ComponentDisplay: React.FC<{
147
146
 
148
147
  if (!!previewFilterResult && previewFilterResult.length > 0) {
149
148
  setPreviewUrl(previewFilterResult[0].url);
150
- !!onValuesChange && onValuesChange(previewFilterResult[0].values);
151
149
  } else {
152
150
  setPreviewUrl(null);
153
151
  }
154
- }, [context.variantFilter, component, onValuesChange]);
152
+ }, [context.variantFilter, component]);
153
+
154
+ React.useEffect(() => {
155
+ if (!component?.previews || !previewUrl) return;
156
+ const selectedPreview = Object.values(component.previews).find((preview) => preview.url === previewUrl);
157
+ if (!selectedPreview) return;
158
+ !!onPreviewChange && onPreviewChange(selectedPreview.url);
159
+ }, [component?.previews, onPreviewChange, previewUrl]);
155
160
 
156
161
  const { reloadCounter } = useContext(HotReloadContext);
157
162
 
@@ -376,7 +381,7 @@ export const ComponentPreview: React.FC<{
376
381
  const context = usePreviewContext();
377
382
  const [loaded, setLoaded] = React.useState(false);
378
383
  const [preview, setPreview] = React.useState<PreviewObject | undefined>(defaultPreview);
379
- const [currentValues, setCurrentValues] = React.useState<Record<string, string> | undefined>();
384
+ const [currentPreviewUrl, setCurrentPreviewUrl] = React.useState<string | undefined>();
380
385
  React.useEffect(() => {
381
386
  if (context.preview) {
382
387
  setPreview(context.preview);
@@ -409,9 +414,9 @@ export const ComponentPreview: React.FC<{
409
414
  preview={preview}
410
415
  title={title}
411
416
  height={height}
412
- currentValues={currentValues}
413
- onValuesChange={(vals) => {
414
- setCurrentValues(vals);
417
+ currentPreviewUrl={currentPreviewUrl}
418
+ onPreviewChange={(previewUrl) => {
419
+ setCurrentPreviewUrl(previewUrl);
415
420
  }}
416
421
  bestPracticesCard={bestPracticesCard}
417
422
  codeHighlight={codeHighlight}
@@ -8,8 +8,8 @@ export interface ComponentDisplaySliceProps {
8
8
  preview: PreviewObject;
9
9
  title: string;
10
10
  height?: string;
11
- currentValues?: Record<string, string>;
12
- onValuesChange?: (values: Record<string, string>) => void;
11
+ currentPreviewUrl?: string;
12
+ onPreviewChange?: (previewUrl: string) => void;
13
13
  showPreview?: boolean;
14
14
  showCodeHighlight?: boolean;
15
15
  defaultHeight?: string;
@@ -33,8 +33,8 @@ const ComponentDisplaySlice: React.FC<ComponentDisplaySliceProps> = ({
33
33
  preview,
34
34
  title,
35
35
  height,
36
- currentValues,
37
- onValuesChange,
36
+ currentPreviewUrl,
37
+ onPreviewChange,
38
38
  showPreview = true,
39
39
  showCodeHighlight,
40
40
  defaultHeight,
@@ -64,12 +64,22 @@ const ComponentDisplaySlice: React.FC<ComponentDisplaySliceProps> = ({
64
64
  return (
65
65
  <div id={filteredPreview.id}>
66
66
  {showPreview && (
67
- <ComponentDisplay title={title} component={filteredPreview} defaultHeight={finalDefaultHeight} onValuesChange={onValuesChange} />
67
+ <ComponentDisplay
68
+ title={title}
69
+ component={filteredPreview}
70
+ defaultHeight={finalDefaultHeight}
71
+ onPreviewChange={onPreviewChange}
72
+ />
68
73
  )}
69
74
  {finalShowCodeHighlight && (
70
75
  <>
71
76
  <a id="code-highlight" />
72
- <CodeHighlight title={title} data={filteredPreview} collapsible={true} currentValues={currentValues} />
77
+ <CodeHighlight
78
+ title={title}
79
+ data={filteredPreview}
80
+ collapsible={true}
81
+ currentPreviewUrl={currentPreviewUrl}
82
+ />
73
83
  </>
74
84
  )}
75
85
  </div>