handoff-app 1.1.3 → 1.2.0-1

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 (160) 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 +31 -12
  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 +106 -0
  13. package/dist/app-builder/config-diff/types.d.ts +33 -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 +65 -79
  21. package/dist/app-builder/websocket.js +10 -4
  22. package/dist/cache/build-cache.js +14 -5
  23. package/dist/cli/create.js +67 -23
  24. package/dist/cli/eject.js +48 -7
  25. package/dist/cli/make.js +29 -4
  26. package/dist/cli/scaffold.js +60 -19
  27. package/dist/cli.js +1 -1
  28. package/dist/config/helpers.d.ts +7 -0
  29. package/dist/config/helpers.js +31 -0
  30. package/dist/config/index.d.ts +2 -1
  31. package/dist/config/index.js +5 -1
  32. package/dist/config/loader.d.ts +10 -1
  33. package/dist/config/loader.js +92 -28
  34. package/dist/config/normalizers/declaration.d.ts +11 -0
  35. package/dist/config/normalizers/declaration.js +119 -0
  36. package/dist/config/normalizers/pattern.d.ts +8 -0
  37. package/dist/config/normalizers/pattern.js +35 -0
  38. package/dist/config/runtime.d.ts +9 -3
  39. package/dist/config/runtime.js +260 -33
  40. package/dist/declarations/helpers.d.ts +6 -0
  41. package/dist/declarations/helpers.js +19 -0
  42. package/dist/declarations/index.d.ts +2 -0
  43. package/dist/declarations/index.js +9 -0
  44. package/dist/declarations/types.d.ts +60 -0
  45. package/dist/declarations/types.js +2 -0
  46. package/dist/index.d.ts +12 -1
  47. package/dist/index.js +34 -5
  48. package/dist/pipeline/index.d.ts +1 -0
  49. package/dist/pipeline/index.js +3 -1
  50. package/dist/pipeline/patterns.d.ts +8 -0
  51. package/dist/pipeline/patterns.js +21 -0
  52. package/dist/transformers/docgen/index.js +12 -2
  53. package/dist/transformers/plugins/csf-render.js +170 -34
  54. package/dist/transformers/plugins/ssr-render.js +18 -2
  55. package/dist/transformers/preview/component/api.js +16 -7
  56. package/dist/transformers/preview/component/builder.js +61 -8
  57. package/dist/transformers/preview/component/css.d.ts +8 -0
  58. package/dist/transformers/preview/component/css.js +6 -4
  59. package/dist/transformers/preview/component/html.js +14 -5
  60. package/dist/transformers/preview/component/javascript.d.ts +0 -1
  61. package/dist/transformers/preview/component/javascript.js +25 -14
  62. package/dist/transformers/preview/component/previews.d.ts +9 -0
  63. package/dist/transformers/preview/component/previews.js +10 -0
  64. package/dist/transformers/preview/pattern/api.d.ts +11 -0
  65. package/dist/transformers/preview/pattern/api.js +79 -0
  66. package/dist/transformers/preview/pattern/builder.d.ts +17 -0
  67. package/dist/transformers/preview/pattern/builder.js +167 -0
  68. package/dist/transformers/preview/pattern/html.d.ts +19 -0
  69. package/dist/transformers/preview/pattern/html.js +94 -0
  70. package/dist/transformers/preview/types.d.ts +54 -0
  71. package/dist/transformers/utils/csf.d.ts +16 -0
  72. package/dist/transformers/utils/csf.js +30 -0
  73. package/dist/transformers/utils/index.d.ts +1 -0
  74. package/dist/transformers/utils/index.js +2 -0
  75. package/dist/transformers/utils/module.d.ts +5 -0
  76. package/dist/transformers/utils/module.js +26 -13
  77. package/dist/transformers/utils/usage.d.ts +23 -0
  78. package/dist/transformers/utils/usage.js +294 -0
  79. package/dist/transformers/utils/vite-logger.js +4 -4
  80. package/dist/transformers/vite-config.js +2 -0
  81. package/dist/types/config.d.ts +32 -7
  82. package/dist/types/preview.d.ts +2 -0
  83. package/dist/utils/duration.d.ts +2 -0
  84. package/dist/utils/duration.js +6 -0
  85. package/dist/utils/logger.d.ts +42 -5
  86. package/dist/utils/logger.js +135 -15
  87. package/docs/cli.md +3 -3
  88. package/package.json +3 -1
  89. package/src/app/components/Component/PageSliceResolver.tsx +6 -6
  90. package/src/app/components/Component/Preview.tsx +17 -12
  91. package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +16 -6
  92. package/src/app/components/Markdown/CodeHighlight.tsx +60 -11
  93. package/src/app/components/util/index.ts +71 -2
  94. package/src/app/pages/system/pattern/[pattern]/index.tsx +346 -0
  95. package/src/app/pages/system/pattern/index.tsx +14 -0
  96. package/src/app-builder/README.md +37 -9
  97. package/src/app-builder/build.ts +34 -12
  98. package/src/app-builder/config-diff/index.ts +4 -0
  99. package/src/app-builder/config-diff/registry.ts +23 -0
  100. package/src/app-builder/config-diff/snapshot.ts +51 -0
  101. package/src/app-builder/config-diff/strategies/pattern.ts +80 -0
  102. package/src/app-builder/config-diff/types.ts +36 -0
  103. package/src/app-builder/watchers/component.ts +72 -0
  104. package/src/app-builder/watchers/utils.ts +44 -0
  105. package/src/app-builder/watchers.ts +74 -80
  106. package/src/app-builder/websocket.ts +16 -5
  107. package/src/cache/build-cache.ts +14 -5
  108. package/src/cli/create.ts +86 -25
  109. package/src/cli/eject.ts +29 -7
  110. package/src/cli/make.ts +32 -5
  111. package/src/cli/scaffold.ts +68 -21
  112. package/src/cli.ts +1 -1
  113. package/src/config/README.md +10 -1
  114. package/src/config/helpers.ts +43 -0
  115. package/src/config/index.ts +4 -1
  116. package/src/config/loader.ts +110 -31
  117. package/src/config/normalizers/declaration.ts +176 -0
  118. package/src/config/normalizers/pattern.ts +56 -0
  119. package/src/config/runtime.ts +317 -35
  120. package/src/declarations/helpers.ts +36 -0
  121. package/src/declarations/index.ts +19 -0
  122. package/src/declarations/types.ts +70 -0
  123. package/src/index.ts +47 -6
  124. package/src/pipeline/index.ts +1 -0
  125. package/src/pipeline/patterns.ts +12 -0
  126. package/src/transformers/README.md +2 -2
  127. package/src/transformers/docgen/index.ts +13 -2
  128. package/src/transformers/plugins/csf-render.ts +216 -49
  129. package/src/transformers/plugins/ssr-render.ts +23 -7
  130. package/src/transformers/preview/component/api.ts +27 -7
  131. package/src/transformers/preview/component/builder.ts +82 -9
  132. package/src/transformers/preview/component/css.ts +6 -5
  133. package/src/transformers/preview/component/html.ts +18 -5
  134. package/src/transformers/preview/component/javascript.ts +29 -19
  135. package/src/transformers/preview/component/previews.ts +12 -0
  136. package/src/transformers/preview/pattern/api.ts +83 -0
  137. package/src/transformers/preview/pattern/builder.ts +192 -0
  138. package/src/transformers/preview/pattern/html.ts +107 -0
  139. package/src/transformers/preview/types.ts +58 -1
  140. package/src/transformers/utils/csf.ts +48 -0
  141. package/src/transformers/utils/index.ts +3 -0
  142. package/src/transformers/utils/module.ts +33 -18
  143. package/src/transformers/utils/usage.ts +319 -0
  144. package/src/transformers/utils/vite-logger.ts +4 -4
  145. package/src/transformers/vite-config.ts +2 -0
  146. package/src/types/README.md +1 -1
  147. package/src/types/config.ts +33 -7
  148. package/src/types/preview.ts +2 -0
  149. package/src/utils/duration.ts +3 -0
  150. package/src/utils/logger.ts +143 -16
  151. package/templates/default/components/button/Button.jsx.tpl +64 -0
  152. package/templates/default/components/button/button.react.handoff.js.tpl +31 -0
  153. package/templates/default/components/button/button.react.handoff.ts.tpl +31 -0
  154. package/templates/default/handoff.config.js.tpl +29 -16
  155. package/templates/default/handoff.config.ts.tpl +196 -0
  156. package/templates/default/handoff.config.with-examples.js.tpl +28 -17
  157. package/templates/default/handoff.config.with-examples.ts.tpl +195 -0
  158. package/templates/default/package.json.tpl +1 -3
  159. package/handoff.config.js +0 -90
  160. package/templates/default/components/button/button.js.tpl +0 -62
@@ -4,43 +4,163 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Logger = void 0;
7
+ const readline_1 = __importDefault(require("readline"));
7
8
  const chalk_1 = __importDefault(require("chalk"));
9
+ /**
10
+ * Env (e.g. project `.env` via `dotenv/config`):
11
+ * - `HANDOFF_LOG_LEVEL` — `debug` | `info` | `warn` | `error` | `silent` (default: `info`).
12
+ * Controls minimum severity: at `warn`, info/success/debug lines are hidden; at `error`, only errors.
13
+ * - `HANDOFF_LOG_SCOPES` — comma-separated allowlist: `handoff`, `vite`, `next`. Defaults to all three if unset or empty.
14
+ *
15
+ * Programmatic `Logger.init({ debug: true })` (Handoff `debug` flag) forces debug-level verbosity unless overridden.
16
+ */
8
17
  class Logger {
9
18
  static init(options) {
19
+ this.applyEnvConfig();
10
20
  if ((options === null || options === void 0 ? void 0 : options.debug) !== undefined) {
11
21
  this.debugMode = options.debug;
12
22
  }
23
+ if ((options === null || options === void 0 ? void 0 : options.debug) === true) {
24
+ this.severityThreshold = 0;
25
+ }
26
+ }
27
+ /** Re-read `process.env` (useful if env is loaded after first `init`). */
28
+ static applyEnvConfig() {
29
+ var _a, _b;
30
+ this.severityThreshold = 1;
31
+ const allScopes = ['handoff', 'vite', 'next'];
32
+ this.scopesFilter = new Set(allScopes);
33
+ const levelRaw = (_a = process.env.HANDOFF_LOG_LEVEL) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
34
+ const levelMap = {
35
+ debug: 0,
36
+ info: 1,
37
+ warn: 2,
38
+ error: 3,
39
+ silent: 99,
40
+ };
41
+ if (levelRaw && levelRaw in levelMap) {
42
+ this.severityThreshold = levelMap[levelRaw];
43
+ }
44
+ const scopesRaw = (_b = process.env.HANDOFF_LOG_SCOPES) === null || _b === void 0 ? void 0 : _b.trim();
45
+ if (scopesRaw) {
46
+ const set = new Set();
47
+ for (const part of scopesRaw.split(',')) {
48
+ const s = part.trim().toLowerCase();
49
+ if (s === 'handoff' || s === 'vite' || s === 'next') {
50
+ set.add(s);
51
+ }
52
+ }
53
+ if (set.size > 0) {
54
+ this.scopesFilter = set;
55
+ }
56
+ }
57
+ }
58
+ static scopeEnabled(scope) {
59
+ return this.scopesFilter.has(scope);
60
+ }
61
+ /** Message severities: debug=0, info-tier=1, warn=2, error=3 */
62
+ static shouldEmit(severity) {
63
+ return severity >= this.severityThreshold;
13
64
  }
14
65
  static getTimestamp() {
15
66
  const now = new Date();
16
67
  return chalk_1.default.gray(`[${now.toISOString()}]`);
17
68
  }
18
- static log(message) {
19
- console.log(`${this.getTimestamp()} ${message}`);
69
+ /** Short label shown after the timestamp so you can scan for handoff vs vite vs next. */
70
+ static scopeTag(scope) {
71
+ const tags = {
72
+ handoff: chalk_1.default.blue.bold('[handoff]'),
73
+ vite: chalk_1.default.magenta.bold('[vite]'),
74
+ next: chalk_1.default.green.bold('[next]'),
75
+ };
76
+ return tags[scope];
20
77
  }
21
- static info(message) {
22
- console.log(`${this.getTimestamp()} ${chalk_1.default.cyan(message)}`);
78
+ static basePrefix(scope) {
79
+ return `${this.getTimestamp()} ${this.scopeTag(scope)}`;
23
80
  }
24
- static success(message) {
25
- console.log(`${this.getTimestamp()} ${chalk_1.default.green(message)}`);
81
+ static log(message, scope = 'handoff') {
82
+ if (!this.scopeEnabled(scope) || !this.shouldEmit(1))
83
+ return;
84
+ console.log(`${this.basePrefix(scope)} ${message}`);
26
85
  }
27
- static warn(message) {
28
- console.warn(`${this.getTimestamp()} ${chalk_1.default.yellow(message)}`);
86
+ /**
87
+ * Vite often passes strings that already contain ANSI; avoid wrapping those in chalk.cyan so colors stay correct.
88
+ */
89
+ static info(message, scope = 'handoff') {
90
+ if (!this.scopeEnabled(scope) || !this.shouldEmit(1))
91
+ return;
92
+ const body = scope === 'vite' ? message : chalk_1.default.cyan(message);
93
+ console.log(`${this.basePrefix(scope)} ${body}`);
29
94
  }
30
- static error(message, error) {
31
- console.error(`${this.getTimestamp()} ${chalk_1.default.red(message)}`);
95
+ static success(message, scope = 'handoff') {
96
+ if (!this.scopeEnabled(scope) || !this.shouldEmit(1))
97
+ return;
98
+ console.log(`${this.basePrefix(scope)} ${chalk_1.default.green(message)}`);
99
+ }
100
+ static warn(message, scope = 'handoff') {
101
+ if (!this.scopeEnabled(scope) || !this.shouldEmit(2))
102
+ return;
103
+ const body = scope === 'vite' ? message : chalk_1.default.yellow(message);
104
+ console.warn(`${this.basePrefix(scope)} ${body}`);
105
+ }
106
+ static error(message, error, scope = 'handoff') {
107
+ if (!this.scopeEnabled(scope) || !this.shouldEmit(3))
108
+ return;
109
+ const body = scope === 'vite' ? message : chalk_1.default.red(message);
110
+ console.error(`${this.basePrefix(scope)} ${body}`);
32
111
  if (error) {
33
112
  console.error(error);
34
113
  }
35
114
  }
36
115
  static debug(message, data) {
37
- if (this.debugMode) {
38
- console.log(`${this.getTimestamp()} ${chalk_1.default.gray(`[DEBUG] ${message}`)}`);
39
- if (data) {
40
- console.log(data);
41
- }
116
+ if (this.severityThreshold >= 99)
117
+ return;
118
+ if (!this.scopeEnabled('handoff'))
119
+ return;
120
+ const allowDebug = this.debugMode || this.severityThreshold === 0;
121
+ if (!allowDebug)
122
+ return;
123
+ console.log(`${this.basePrefix('handoff')} ${chalk_1.default.gray(`[DEBUG] ${message}`)}`);
124
+ if (data) {
125
+ console.log(data);
126
+ }
127
+ }
128
+ /**
129
+ * Log one line from a child process with timestamp + scope tag. Child ANSI is preserved (no extra chalk on the line).
130
+ */
131
+ static childProcessLine(line, scope = 'next') {
132
+ if (!this.scopeEnabled(scope) || !this.shouldEmit(1))
133
+ return;
134
+ console.log(`${this.basePrefix(scope)} ${line}`);
135
+ }
136
+ /**
137
+ * Pipe stdout/stderr from a spawned child through {@link childProcessLine}. Use with
138
+ * `stdio: ['inherit' | 'ignore', 'pipe', 'pipe']`.
139
+ */
140
+ static pipeChildStreams(stdout, stderr, scope = 'next') {
141
+ for (const stream of [stdout, stderr]) {
142
+ if (!stream)
143
+ continue;
144
+ const rl = readline_1.default.createInterface({ input: stream, crlfDelay: Infinity });
145
+ rl.on('line', (line) => {
146
+ this.childProcessLine(line, scope);
147
+ });
148
+ }
149
+ }
150
+ /** Forward a completed child-process buffer (e.g. from `spawn.sync` with stdio pipe) line-by-line. */
151
+ static childProcessBuffer(buffer, scope = 'next') {
152
+ if (!(buffer === null || buffer === void 0 ? void 0 : buffer.length))
153
+ return;
154
+ const text = buffer.toString('utf8');
155
+ for (const line of text.split(/\r?\n/)) {
156
+ this.childProcessLine(line, scope);
42
157
  }
43
158
  }
44
159
  }
45
160
  exports.Logger = Logger;
161
+ /** From Handoff constructor; enables debug lines together with env. */
46
162
  Logger.debugMode = false;
163
+ /** Minimum message severity to print: 0=debug, 1=info, 2=warn, 3=error. `silent` uses 99. */
164
+ Logger.severityThreshold = 1;
165
+ /** Enabled scopes; defaults to all of `handoff`, `vite`, `next`. */
166
+ Logger.scopesFilter = new Set(['handoff', 'vite', 'next']);
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-1",
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>
@@ -2,7 +2,7 @@ import { PreviewObject } from '@handoff/types/preview';
2
2
  // @ts-ignore
3
3
  import { CollapsibleTrigger } from '@radix-ui/react-collapsible';
4
4
  import { Select } from '@radix-ui/react-select';
5
- import { useEffect, useState } from 'react';
5
+ import { useEffect, useMemo, useState } from 'react';
6
6
  import handlebars from 'react-syntax-highlighter/dist/esm/languages/prism/handlebars';
7
7
  import js from 'react-syntax-highlighter/dist/esm/languages/prism/javascript';
8
8
  import json from 'react-syntax-highlighter/dist/esm/languages/prism/json';
@@ -47,8 +47,8 @@ export const CodeHighlight: React.FC<{
47
47
  title?: string;
48
48
  language?: string;
49
49
  height?: string;
50
- currentValues?: Record<string, string>;
51
- }> = ({ data, collapsible, type, title, dark, height, currentValues }) => {
50
+ currentPreviewUrl?: string;
51
+ }> = ({ data, collapsible, type, title, dark, height, currentPreviewUrl }) => {
52
52
  const [isOpen, setIsOpen] = useState<boolean>(true);
53
53
 
54
54
  if (!data) {
@@ -69,7 +69,7 @@ export const CodeHighlight: React.FC<{
69
69
  format: 'html',
70
70
  };
71
71
  } else if (typeof data === 'string') {
72
- data = data = {
72
+ data = {
73
73
  id: '',
74
74
  title: '',
75
75
  categories: [],
@@ -91,13 +91,26 @@ export const CodeHighlight: React.FC<{
91
91
  const metadataKeys = [
92
92
  'id', 'preview', 'image', 'categories', 'title', 'format',
93
93
  'description', 'type', 'group', 'tags', 'previews', 'properties',
94
- 'should_do', 'should_not_do', 'figma',
94
+ 'should_do', 'should_not_do', 'figma', 'usage',
95
95
  ];
96
96
 
97
97
  const states = Object.keys(data)
98
98
  .filter((key) => !metadataKeys.includes(key) && !!(data as Record<string, any>)[key]);
99
- const [activeState, setActiveState] = useState<string>(states[0]);
99
+ const hasPerPreviewUsage =
100
+ typeof data === 'object' &&
101
+ !!data.previews &&
102
+ Object.values(data.previews).some((preview) => !!preview?.usage);
103
+ const hasUsageState = (typeof data === 'object' && !!data.usage) || hasPerPreviewUsage;
104
+ const selectableStates = hasUsageState && !states.includes('usage') ? ['usage', ...states] : states;
105
+ const preferredState = selectableStates.includes('usage') ? 'usage' : selectableStates[0];
106
+ const [activeState, setActiveState] = useState<string>(preferredState);
100
107
  const [code, setCode] = useState<string>(data.html);
108
+ const stateResetKey = useMemo(() => {
109
+ if (typeof data === 'string') {
110
+ return `string:${type || 'html'}`;
111
+ }
112
+ return `${data.id || ''}|${data.title || ''}|${data.format || ''}|${Object.keys(data.previews || {}).join('|')}`;
113
+ }, [data, type]);
101
114
  const theme = dark ? oneDark : oneLight;
102
115
  theme['pre[class*="language-"]'].overflow = 'auto';
103
116
  theme['pre[class*="language-"]'].maxHeight = height ?? '450px';
@@ -118,6 +131,7 @@ export const CodeHighlight: React.FC<{
118
131
  sass: 'SASS',
119
132
  scss: 'SCSS',
120
133
  sharedStyles: 'Shared CSS',
134
+ usage: 'Usage',
121
135
  json: 'JSON',
122
136
  markdown: 'Markdown',
123
137
  };
@@ -167,6 +181,7 @@ export const CodeHighlight: React.FC<{
167
181
  css: 'css',
168
182
  sass: 'scss',
169
183
  sharedStyles: 'css',
184
+ usage: 'tsx',
170
185
  json: 'json',
171
186
  handlebars: 'handlebars',
172
187
  hbs: 'handlebars',
@@ -195,12 +210,43 @@ export const CodeHighlight: React.FC<{
195
210
  return jsxPatterns.some(pattern => pattern.test(code));
196
211
  };
197
212
 
213
+ const resolveUsageCode = (): string => {
214
+ if (typeof data !== 'object') {
215
+ return '';
216
+ }
217
+
218
+ if (currentPreviewUrl && data.previews) {
219
+ const activePreview = Object.values(data.previews).find((preview) => preview.url === currentPreviewUrl);
220
+ if (activePreview?.usage) {
221
+ return activePreview.usage;
222
+ }
223
+ }
224
+
225
+ const firstPreviewWithUsage = data.previews ? Object.values(data.previews).find((preview) => !!preview.usage) : undefined;
226
+ if (firstPreviewWithUsage?.usage) {
227
+ return firstPreviewWithUsage.usage;
228
+ }
229
+
230
+ return data.usage || '';
231
+ };
232
+
233
+ // Always default to usage for newly viewed component/code payloads.
234
+ // Users can still manually switch tabs after this initial selection.
235
+ useEffect(() => {
236
+ setActiveState(preferredState);
237
+ }, [preferredState, stateResetKey]);
238
+
198
239
  useEffect(() => {
199
240
  if (typeof data === 'string') {
200
241
  setCode(data);
201
242
  return;
202
243
  }
203
244
 
245
+ if (activeState === 'usage') {
246
+ setCode(resolveUsageCode());
247
+ return;
248
+ }
249
+
204
250
  if (activeState === 'code' && 'code' in data && !!data.code) {
205
251
  setCode(data.code);
206
252
  return;
@@ -209,7 +255,7 @@ export const CodeHighlight: React.FC<{
209
255
  if (activeState in data && !!(data as Record<string, any>)[activeState]) {
210
256
  setCode((data as Record<string, any>)[activeState]);
211
257
  }
212
- }, [activeState, currentValues, data]);
258
+ }, [activeState, currentPreviewUrl, data]);
213
259
 
214
260
  return (
215
261
  <Collapsible id="code-samples" className="mt-4 space-y-2" style={{ maxWidth: '71vw' }} open={isOpen} onOpenChange={setIsOpen}>
@@ -219,14 +265,17 @@ export const CodeHighlight: React.FC<{
219
265
  >
220
266
  {title && <div>{title}</div>}
221
267
  <div className="flex items-center gap-2">
222
- {states.length > 2 && (
268
+ {selectableStates.length > 1 && (
223
269
  <Select
224
- defaultValue={activeState}
270
+ value={activeState}
225
271
  onValueChange={(key) => {
226
272
  setActiveState(key);
227
273
  if (typeof data === 'string') {
228
274
  setCode(data);
229
275
  return;
276
+ } else if (key === 'usage') {
277
+ setCode(resolveUsageCode());
278
+ return;
230
279
  } else {
231
280
  setCode(data[key]);
232
281
  }
@@ -236,8 +285,8 @@ export const CodeHighlight: React.FC<{
236
285
  <SelectValue placeholder="Code View" />
237
286
  </SelectTrigger>
238
287
  <SelectContent>
239
- {states
240
- .filter((value) => ['code', 'html', 'css', 'js', 'sass', 'sharedStyles'].includes(value))
288
+ {selectableStates
289
+ .filter((value) => ['usage', 'code', 'html', 'css', 'js', 'sass', 'sharedStyles'].includes(value))
241
290
  .map((state) => {
242
291
  return (
243
292
  <SelectItem key={state} value={state}>
@@ -1,4 +1,4 @@
1
- import { ComponentListObject, ComponentType } from '@handoff/transformers/preview/types';
1
+ import { ComponentListObject, ComponentType, PatternListObject } from '@handoff/transformers/preview/types';
2
2
  import { ClientConfig, RuntimeConfig } from '@handoff/types/config';
3
3
  import { ComponentDocumentationOptions, PreviewObject } from '@handoff/types/preview';
4
4
  import * as fs from 'fs-extra';
@@ -85,7 +85,7 @@ export const knownPaths = [
85
85
  'assets/logos',
86
86
  'foundations',
87
87
  'foundations/colors',
88
- 'foundations/icons',
88
+ 'foundations/icons',
89
89
  'foundations/effects',
90
90
  'foundations/logos',
91
91
  'foundations/logo',
@@ -98,6 +98,7 @@ export const knownPaths = [
98
98
  'system/tokens/foundations/effects',
99
99
  'system/tokens/foundations/typography',
100
100
  'system/tokens/components',
101
+ 'system/pattern',
101
102
  ];
102
103
 
103
104
  /**
@@ -266,6 +267,16 @@ export const staticBuildMenu = () => {
266
267
  menu: staticBuildTokensMenu(),
267
268
  };
268
269
  }
270
+ if (sub.patterns) {
271
+ const patternMenu = staticBuildPatternMenu();
272
+ if (patternMenu.length > 0) {
273
+ return {
274
+ title: sub.title || 'Patterns',
275
+ menu: patternMenu,
276
+ };
277
+ }
278
+ return undefined;
279
+ }
269
280
  if (sub.enabled !== false) {
270
281
  return sub;
271
282
  }
@@ -413,6 +424,64 @@ const staticBuildTokenMenu = () => {
413
424
  return subSections;
414
425
  };
415
426
 
427
+ /**
428
+ * Fetch patterns from the patterns.json API file
429
+ */
430
+ export const fetchPatterns = (): { id: string; title: string; description: string; group: string }[] => {
431
+ const patternsFilePath = path.resolve(
432
+ process.env.HANDOFF_MODULE_PATH ?? '',
433
+ '.handoff',
434
+ `${process.env.HANDOFF_PROJECT_ID}`,
435
+ 'public',
436
+ 'api',
437
+ 'patterns.json'
438
+ );
439
+
440
+ if (!fs.existsSync(patternsFilePath)) {
441
+ return [];
442
+ }
443
+
444
+ try {
445
+ const patternList = JSON.parse(
446
+ fs.readFileSync(patternsFilePath, 'utf-8')
447
+ ) as PatternListObject[];
448
+
449
+ return patternList.map((p) => ({
450
+ id: p.id,
451
+ title: p.title || '',
452
+ description: p.description || '',
453
+ group: p.group || '',
454
+ }));
455
+ } catch {
456
+ return [];
457
+ }
458
+ };
459
+
460
+ const staticBuildPatternMenu = () => {
461
+ const basePath = buildBasePath();
462
+ const patterns = fetchPatterns();
463
+
464
+ if (patterns.length === 0) return [];
465
+
466
+ const grouped = groupBy(patterns, (p) => p.group ?? '');
467
+ let menu: { title: string; menu: { path: string; title: string }[] }[] = [];
468
+
469
+ Object.keys(grouped).forEach((group) => {
470
+ const menuGroup = { title: group || 'Uncategorized', menu: [] as { path: string; title: string }[] };
471
+ grouped[group].forEach((pattern) => {
472
+ menuGroup.menu.push({
473
+ path: `${basePath}system/pattern/${pattern.id}`,
474
+ title: pattern.title || startCase(pattern.id),
475
+ });
476
+ });
477
+ menuGroup.menu.sort((a, b) => a.title.localeCompare(b.title));
478
+ menu.push(menuGroup);
479
+ });
480
+
481
+ menu.sort((a, b) => a.title.localeCompare(b.title));
482
+ return menu;
483
+ };
484
+
416
485
  /**
417
486
  * Filter the menus by the current path
418
487
  * @param menu