@teambit/api-reference 1.0.1079 → 1.0.1081

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.
@@ -3,248 +3,6 @@
3
3
  flex-direction: column;
4
4
  height: 100%;
5
5
  overflow: auto;
6
- padding: 24px;
7
- gap: 16px;
8
- }
9
-
10
- .summary {
11
- display: flex;
12
- gap: 12px;
13
- align-items: center;
14
- padding: 12px 16px;
15
- background: var(--bit-bg-dent, #f6f6f6);
16
- border-radius: 8px;
17
- font-size: 13px;
18
- }
19
-
20
- .summaryDivider {
21
- opacity: 0.3;
22
- color: var(--bit-text-color-light, #6c707c);
23
- }
24
-
25
- .summaryBadge {
26
- display: inline-flex;
27
- align-items: center;
28
- gap: 4px;
29
- padding: 3px 10px;
30
- border-radius: 12px;
31
- font-weight: 500;
32
- font-size: 12px;
33
- white-space: nowrap;
34
- }
35
-
36
- .addedBadge {
37
- composes: summaryBadge;
38
- background: color-mix(in srgb, var(--bit-color-added, #2ea043) 15%, transparent);
39
- color: var(--bit-color-added, #2ea043);
40
- }
41
-
42
- .removedBadge {
43
- composes: summaryBadge;
44
- background: color-mix(in srgb, var(--bit-color-removed, #f85149) 15%, transparent);
45
- color: var(--bit-color-removed, #f85149);
46
- }
47
-
48
- .modifiedBadge {
49
- composes: summaryBadge;
50
- background: color-mix(in srgb, var(--bit-color-modified, #d29922) 15%, transparent);
51
- color: var(--bit-color-modified, #d29922);
52
- }
53
-
54
- .changeList {
55
- display: flex;
56
- flex-direction: column;
57
- gap: 8px;
58
- }
59
-
60
- .sectionTitle {
61
- margin: 16px 0 8px;
62
- font-size: 12px;
63
- font-weight: 600;
64
- text-transform: uppercase;
65
- letter-spacing: 0.5px;
66
- color: var(--bit-text-color-light, #6c707c);
67
- }
68
-
69
- .diffEntry {
70
- border: 1px solid var(--bit-border-color-lightest, #ededed);
71
- border-radius: 8px;
72
- overflow: hidden;
73
- }
74
-
75
- .diffEntryHeader {
76
- display: flex;
77
- align-items: center;
78
- gap: 10px;
79
- padding: 10px 16px;
80
- cursor: pointer;
81
- user-select: none;
82
-
83
- &:hover {
84
- background: var(--bit-bg-dent, #f6f6f6);
85
- }
86
- }
87
-
88
- .statusBadge {
89
- display: inline-flex;
90
- align-items: center;
91
- padding: 2px 8px;
92
- border-radius: 4px;
93
- font-weight: 600;
94
- font-size: 11px;
95
- text-transform: uppercase;
96
- letter-spacing: 0.3px;
97
- flex-shrink: 0;
98
- white-space: nowrap;
99
- }
100
-
101
- .statusAdded {
102
- composes: statusBadge;
103
- background: color-mix(in srgb, var(--bit-color-added, #2ea043) 15%, transparent);
104
- color: var(--bit-color-added, #2ea043);
105
- }
106
-
107
- .statusRemoved {
108
- composes: statusBadge;
109
- background: color-mix(in srgb, var(--bit-color-removed, #f85149) 15%, transparent);
110
- color: var(--bit-color-removed, #f85149);
111
- }
112
-
113
- .statusModified {
114
- composes: statusBadge;
115
- background: color-mix(in srgb, var(--bit-color-modified, #d29922) 15%, transparent);
116
- color: var(--bit-color-modified, #d29922);
117
- }
118
-
119
- .exportName {
120
- font-weight: 600;
121
- font-size: 14px;
122
- font-family: var(--bit-font-mono, monospace);
123
- color: var(--bit-text-color-heavy, #2b2b2b);
124
- }
125
-
126
- .schemaType {
127
- color: var(--bit-text-color-light, #6c707c);
128
- font-size: 12px;
129
- }
130
-
131
- .impactBadge {
132
- composes: summaryBadge;
133
- margin-left: auto;
134
- font-size: 11px;
135
- }
136
-
137
- .expandIcon {
138
- color: var(--bit-text-color-light, #6c707c);
139
- font-size: 10px;
140
- transition: transform 0.2s;
141
- flex-shrink: 0;
142
- }
143
-
144
- .expandIconOpen {
145
- composes: expandIcon;
146
- transform: rotate(90deg);
147
- }
148
-
149
- .diffEntryBody {
150
- padding: 0 16px 16px;
151
- border-top: 1px solid var(--bit-border-color-lightest, #ededed);
152
- }
153
-
154
- .detailsList {
155
- list-style: none;
156
- padding: 12px 0 0;
157
- margin: 0;
158
- display: flex;
159
- flex-direction: column;
160
- gap: 8px;
161
- }
162
-
163
- .detailItem {
164
- display: flex;
165
- align-items: flex-start;
166
- gap: 8px;
167
- font-size: 13px;
168
- line-height: 1.5;
169
- color: var(--bit-text-color-heavy, #2b2b2b);
170
- }
171
-
172
- .detailDot {
173
- width: 7px;
174
- height: 7px;
175
- min-width: 7px;
176
- border-radius: 50%;
177
- margin-top: 6px;
178
- flex-shrink: 0;
179
- }
180
-
181
- .dotBreaking {
182
- composes: detailDot;
183
- background: var(--bit-color-removed, #f85149);
184
- }
185
-
186
- .dotNonBreaking {
187
- composes: detailDot;
188
- background: var(--bit-color-added, #2ea043);
189
- }
190
-
191
- .dotPatch {
192
- composes: detailDot;
193
- background: var(--bit-color-modified, #d29922);
194
- }
195
-
196
- .detailDescription {
197
- color: var(--bit-text-color-heavy, #2b2b2b);
198
- }
199
-
200
- .signatureBlock {
201
- margin-top: 12px;
202
- padding: 12px;
203
- background: var(--bit-bg-dent, #f6f6f6);
204
- border-radius: 6px;
205
- font-family: var(--bit-font-mono, monospace);
206
- font-size: 12px;
207
- white-space: pre-wrap;
208
- word-break: break-word;
209
- line-height: 1.6;
210
- color: var(--bit-text-color-heavy, #2b2b2b);
211
- }
212
-
213
- .signatureLabel {
214
- font-weight: 600;
215
- color: var(--bit-text-color-light, #6c707c);
216
- margin-top: 12px;
217
- margin-bottom: 4px;
218
- font-size: 11px;
219
- text-transform: uppercase;
220
- letter-spacing: 0.5px;
221
- }
222
-
223
- .signatureRemoved {
224
- composes: signatureBlock;
225
- text-decoration: line-through;
226
- opacity: 0.6;
227
- }
228
-
229
- .emptyState {
230
- display: flex;
231
- align-items: center;
232
- justify-content: center;
233
- height: 100%;
234
- color: var(--bit-text-color-light, #6c707c);
235
- font-size: 14px;
236
- }
237
-
238
- .toggleSignatures {
239
- background: none;
240
- border: none;
241
- color: var(--bit-accent-color, #0366d6);
242
- font-size: 12px;
243
- cursor: pointer;
244
- padding: 4px 0;
245
- margin-top: 12px;
246
-
247
- &:hover {
248
- text-decoration: underline;
249
- }
6
+ padding: 16px 24px;
7
+ max-width: 980px;
250
8
  }
@@ -2,12 +2,24 @@ import React from 'react';
2
2
  import type { Section } from '@teambit/component';
3
3
  import type { TabItem } from '@teambit/component.ui.component-compare.models.component-compare-props';
4
4
  import type { ChangeType } from '@teambit/component.ui.component-compare.models.component-compare-change-type';
5
+ import type { ApiDiffInsight } from '@teambit/semantics.ui.api-diff-view';
5
6
  import { APICompare } from './api-compare';
6
7
 
7
8
  // Use string cast since the external ChangeType enum doesn't have API yet
8
9
  const ChangeTypeAPI = 'API' as unknown as ChangeType;
9
10
 
10
11
  export class APICompareSection implements TabItem, Section {
12
+ route: { path: string; element: React.ReactNode };
13
+
14
+ constructor(getInsights?: () => ApiDiffInsight[]) {
15
+ // built in the constructor (not a field initializer) so the element captures the
16
+ // getter regardless of class-field initialization order.
17
+ this.route = {
18
+ path: 'api/*',
19
+ element: <APICompare getInsights={getInsights} />,
20
+ };
21
+ }
22
+
11
23
  navigationLink = {
12
24
  href: 'api',
13
25
  children: 'API',
@@ -15,11 +27,6 @@ export class APICompareSection implements TabItem, Section {
15
27
 
16
28
  props = this.navigationLink;
17
29
 
18
- route = {
19
- path: 'api/*',
20
- element: <APICompare />,
21
- };
22
-
23
30
  order = 15;
24
31
  changeType = ChangeTypeAPI;
25
32
  id = 'api';
package/api-compare.tsx CHANGED
@@ -1,202 +1,84 @@
1
1
  import React from 'react';
2
2
  import { useComponentCompare } from '@teambit/component.ui.component-compare.context';
3
- import type { APIDiffResult, APIDiffChange, APIDiffDetail } from './api-compare.types';
3
+ import {
4
+ ComponentApiDiffSection,
5
+ ApiDiffSlimRow,
6
+ ApiDiffInsightProvider,
7
+ useApiDiff,
8
+ } from '@teambit/semantics.ui.api-diff-view';
9
+ import type { ApiDiffInsight } from '@teambit/semantics.ui.api-diff-view';
4
10
  import styles from './api-compare.module.scss';
5
11
 
6
- export type { APIDiffResult, APIDiffChange, APIDiffDetail };
12
+ // single source of truth for the API diff model lives in api-diff-view.
13
+ export type { APIDiffResult, APIDiffChange, APIDiffDetail } from '@teambit/semantics.ui.api-diff-view';
7
14
 
8
- function impactClass(impact: string): string {
9
- switch (impact) {
10
- case 'BREAKING':
11
- return styles.removedBadge;
12
- case 'NON_BREAKING':
13
- return styles.addedBadge;
14
- case 'PATCH':
15
- return styles.modifiedBadge;
16
- default:
17
- return styles.summaryBadge;
18
- }
19
- }
20
-
21
- function impactLabel(impact: string): string {
22
- switch (impact) {
23
- case 'BREAKING':
24
- return 'Breaking';
25
- case 'NON_BREAKING':
26
- return 'Non-breaking';
27
- case 'PATCH':
28
- return 'Patch';
29
- default:
30
- return impact;
31
- }
32
- }
33
-
34
- function getStatusClass(status: string): string {
35
- switch (status) {
36
- case 'ADDED':
37
- return styles.statusAdded;
38
- case 'REMOVED':
39
- return styles.statusRemoved;
40
- case 'MODIFIED':
41
- return styles.statusModified;
42
- default:
43
- return styles.statusBadge;
44
- }
45
- }
46
-
47
- function getStatusLabel(status: string): string {
48
- switch (status) {
49
- case 'ADDED':
50
- return '+ Added';
51
- case 'REMOVED':
52
- return '- Removed';
53
- case 'MODIFIED':
54
- return '~ Modified';
55
- default:
56
- return status;
57
- }
58
- }
59
-
60
- function dotClass(impact: string): string {
61
- switch (impact) {
62
- case 'BREAKING':
63
- return styles.dotBreaking;
64
- case 'NON_BREAKING':
65
- return styles.dotNonBreaking;
66
- default:
67
- return styles.dotPatch;
68
- }
69
- }
70
-
71
- function DetailItem({ detail }: { detail: APIDiffDetail }) {
72
- return (
73
- <li className={styles.detailItem}>
74
- <span className={dotClass(detail.impact)} />
75
- <span className={styles.detailDescription}>{detail.description}</span>
76
- </li>
77
- );
78
- }
79
-
80
- function APIDiffEntry({ change }: { change: APIDiffChange }) {
81
- const [expanded, setExpanded] = React.useState(change.status === 'MODIFIED');
82
- const [showSignatures, setShowSignatures] = React.useState(false);
15
+ export type APICompareProps = {
16
+ /** resolved lazily at render time — slot registrations can land after the section is constructed */
17
+ getInsights?: () => ApiDiffInsight[];
18
+ };
83
19
 
84
- const hasBody =
85
- change.status === 'MODIFIED'
86
- ? (change.changes && change.changes.length > 0) || change.baseSignature || change.compareSignature
87
- : change.baseSignature || change.compareSignature;
88
-
89
- return (
90
- <div className={styles.diffEntry}>
91
- <div
92
- className={styles.diffEntryHeader}
93
- onClick={() => hasBody && setExpanded(!expanded)}
94
- role="button"
95
- tabIndex={0}
96
- aria-expanded={hasBody ? expanded : undefined}
97
- onKeyDown={(e) => {
98
- if (e.key === 'Enter' || e.key === ' ') {
99
- e.preventDefault();
100
- hasBody && setExpanded(!expanded);
101
- }
102
- }}
103
- >
104
- <span className={getStatusClass(change.status)}>{getStatusLabel(change.status)}</span>
105
- <span className={styles.exportName}>{change.exportName}</span>
106
- <span className={styles.schemaType}>{change.schemaType}</span>
107
- <span className={`${impactClass(change.impact)} ${styles.impactBadge}`}>{impactLabel(change.impact)}</span>
108
- {hasBody && <span className={expanded ? styles.expandIconOpen : styles.expandIcon}>▶</span>}
109
- </div>
110
- {expanded && hasBody && (
111
- <div className={styles.diffEntryBody}>
112
- {change.status === 'MODIFIED' && change.changes && change.changes.length > 0 && (
113
- <ul className={styles.detailsList}>
114
- {change.changes.map((detail, i) => (
115
- <DetailItem key={`${detail.changeKind}-${i}`} detail={detail} />
116
- ))}
117
- </ul>
118
- )}
119
- {change.status === 'MODIFIED' && (change.baseSignature || change.compareSignature) && (
120
- <>
121
- <button className={styles.toggleSignatures} onClick={() => setShowSignatures(!showSignatures)}>
122
- {showSignatures ? '▼ Hide signatures' : '▶ Show signatures'}
123
- </button>
124
- {showSignatures && (
125
- <>
126
- {change.baseSignature && (
127
- <div>
128
- <div className={styles.signatureLabel}>Base</div>
129
- <div className={styles.signatureRemoved}>{change.baseSignature}</div>
130
- </div>
131
- )}
132
- {change.compareSignature && (
133
- <div>
134
- <div className={styles.signatureLabel}>Compare</div>
135
- <div className={styles.signatureBlock}>{change.compareSignature}</div>
136
- </div>
137
- )}
138
- </>
139
- )}
140
- </>
141
- )}
142
- {change.status === 'ADDED' && change.compareSignature && (
143
- <div className={styles.signatureBlock}>{change.compareSignature}</div>
144
- )}
145
- {change.status === 'REMOVED' && change.baseSignature && (
146
- <div className={styles.signatureRemoved}>{change.baseSignature}</div>
147
- )}
148
- </div>
149
- )}
150
- </div>
151
- );
152
- }
153
-
154
- function ChangeSection({ title, changes }: { title: string; changes: APIDiffChange[] }) {
155
- if (changes.length === 0) return null;
156
-
157
- const sorted = [...changes].sort((a, b) => {
158
- const order: Record<string, number> = { REMOVED: 0, MODIFIED: 1, ADDED: 2 };
159
- return (order[a.status] ?? 3) - (order[b.status] ?? 3);
20
+ /**
21
+ * the API tab of the single-component compare page. thin host around the same
22
+ * `ComponentApiDiffSection` the lane-compare API view uses one implementation, two hosts.
23
+ * data comes from the compare context (fetched once by ComponentCompare).
24
+ */
25
+ export function APICompare({ getInsights }: APICompareProps) {
26
+ const compareContext = useComponentCompare();
27
+ const baseModel = compareContext?.base?.model;
28
+ const compareModel = compareContext?.compare?.model;
29
+
30
+ // Prefer a diff already on the context — the legacy `ComponentCompare` fetches it once and sets
31
+ // it there. The redesign single-component page does NOT put it on its inline context, so we fetch
32
+ // on our own. This hook is only mounted when the API view is the active tab (the page renders the
33
+ // element on-demand), so lane-compare — which renders one panel per component and never mounts
34
+ // this tab pays nothing.
35
+ const contextApiDiff = compareContext?.apiDiffResult;
36
+ const shouldSelfFetch = contextApiDiff === undefined;
37
+ const { result: fetchedApiDiff } = useApiDiff(baseModel?.id.toString(), compareModel?.id.toString(), {
38
+ skip: !shouldSelfFetch,
160
39
  });
40
+ const apiDiffResult = shouldSelfFetch ? fetchedApiDiff : contextApiDiff;
161
41
 
162
- return (
163
- <>
164
- <h3 className={styles.sectionTitle}>{title}</h3>
165
- <div className={styles.changeList}>
166
- {sorted.map((change) => (
167
- <APIDiffEntry key={`${change.visibility}-${change.status}-${change.exportName}`} change={change} />
168
- ))}
169
- </div>
170
- </>
171
- );
172
- }
42
+ // memoized so the insights context value keeps a stable identity — otherwise a fresh array every
43
+ // render would re-render every `useApiDiffInsights` consumer (incl. the React.memo'd ApiChangeBlock).
44
+ const insights = React.useMemo(() => getInsights?.(), [getInsights]);
173
45
 
174
- export function APICompare() {
175
- const compareContext = useComponentCompare();
176
- const apiDiffResult: APIDiffResult | null | undefined = (compareContext as any)?.apiDiffResult;
46
+ if (!compareContext) return null;
177
47
 
178
- if (apiDiffResult === undefined) {
179
- return <div className={styles.emptyState}>Loading API diff...</div>;
180
- }
48
+ const id = (compareModel || baseModel)?.id;
49
+ if (!id) return null;
181
50
 
182
- if (!apiDiffResult || !apiDiffResult.hasChanges) {
183
- return <div className={styles.emptyState}>No API changes between these versions</div>;
184
- }
51
+ const componentIdStr = id.toStringWithoutVersion();
52
+ const baseVersion = baseModel?.id.version;
53
+ const compareVersion = compareModel?.id.version;
185
54
 
186
- const { publicChanges, internalChanges, added, removed, modified, breaking, impact } = apiDiffResult;
55
+ // no base (first version) or identical versions — nothing to diff, not a failure.
56
+ const nothingToCompare = apiDiffResult === null && (!baseVersion || baseVersion === compareVersion);
187
57
 
188
58
  return (
189
- <div className={styles.apiCompareContainer}>
190
- <div className={styles.summary}>
191
- <span className={impactClass(impact)}>{impactLabel(impact)}</span>
192
- <span className={styles.summaryDivider}>|</span>
193
- {added > 0 && <span className={styles.addedBadge}>+{added} added</span>}
194
- {removed > 0 && <span className={styles.removedBadge}>-{removed} removed</span>}
195
- {modified > 0 && <span className={styles.modifiedBadge}>~{modified} modified</span>}
196
- {breaking > 0 && <span className={styles.removedBadge}>{breaking} breaking</span>}
59
+ <ApiDiffInsightProvider insights={insights}>
60
+ <div className={styles.apiCompareContainer}>
61
+ {nothingToCompare ? (
62
+ <ApiDiffSlimRow
63
+ componentIdStr={componentIdStr}
64
+ displayName={id.fullName}
65
+ chip="nothing to compare"
66
+ detail={baseVersion ? 'both sides are the same version' : 'no base version to compare against'}
67
+ tone="ok"
68
+ />
69
+ ) : (
70
+ <ComponentApiDiffSection
71
+ componentIdStr={componentIdStr}
72
+ displayName={id.fullName}
73
+ baseId={baseModel?.id.toString()}
74
+ compareId={compareModel?.id.toString()}
75
+ baseVersion={baseVersion}
76
+ compareVersion={compareVersion}
77
+ result={apiDiffResult}
78
+ loading={apiDiffResult === undefined}
79
+ />
80
+ )}
197
81
  </div>
198
- <ChangeSection title="Public API" changes={publicChanges} />
199
- <ChangeSection title="Internal (non-exported)" changes={internalChanges} />
200
- </div>
82
+ </ApiDiffInsightProvider>
201
83
  );
202
84
  }
@@ -87,8 +87,9 @@ export class APIReferenceUI {
87
87
  // register all default schema classes
88
88
  apiReferenceUI.registerSchemaClasses(() => Object.values(Schemas));
89
89
 
90
- // register API compare tab in component compare
91
- const apiCompareSection = new APICompareSection();
90
+ // register API compare tab in component compare. insights are resolved lazily —
91
+ // aspects can register them on componentCompareUI after this provider runs.
92
+ const apiCompareSection = new APICompareSection(() => componentCompareUI.getApiDiffInsights());
92
93
  componentCompareUI.registerNavigation(apiCompareSection);
93
94
  componentCompareUI.registerRoutes([apiCompareSection.route]);
94
95
 
@@ -1,4 +1,13 @@
1
1
  import React from 'react';
2
- import type { APIDiffResult, APIDiffChange, APIDiffDetail } from './api-compare.types';
3
- export type { APIDiffResult, APIDiffChange, APIDiffDetail };
4
- export declare function APICompare(): React.JSX.Element;
2
+ import type { ApiDiffInsight } from '@teambit/semantics.ui.api-diff-view';
3
+ export type { APIDiffResult, APIDiffChange, APIDiffDetail } from '@teambit/semantics.ui.api-diff-view';
4
+ export type APICompareProps = {
5
+ /** resolved lazily at render time — slot registrations can land after the section is constructed */
6
+ getInsights?: () => ApiDiffInsight[];
7
+ };
8
+ /**
9
+ * the API tab of the single-component compare page. thin host around the same
10
+ * `ComponentApiDiffSection` the lane-compare API view uses — one implementation, two hosts.
11
+ * data comes from the compare context (fetched once by ComponentCompare).
12
+ */
13
+ export declare function APICompare({ getInsights }: APICompareProps): React.JSX.Element | null;