@staffbase/design 18.8.0 → 19.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.
- package/codemods/v19-migrate-button/README.md +94 -0
- package/codemods/v19-migrate-button/transform.js +277 -0
- package/dist/components.css +1 -1
- package/dist/main.cjs +84 -66
- package/dist/main.cjs.map +1 -1
- package/dist/main.d.ts +22 -5
- package/dist/main.js +84 -67
- package/dist/main.js.map +1 -1
- package/dist/theme.css +38 -29
- package/dist/tokens/component.css +36 -27
- package/package.json +2 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# v19 — Migrate to the new Button
|
|
2
|
+
|
|
3
|
+
This codemod automatically migrates your code from the four deprecated button components in `@staffbase/design` v18 to the new unified `Button` in v19.
|
|
4
|
+
|
|
5
|
+
## What it transforms
|
|
6
|
+
|
|
7
|
+
| Before (v18) | After (v19) |
|
|
8
|
+
| ----------------------------------- | ------------------------------------------------- |
|
|
9
|
+
| `<Button variant="primary">` | `<Button>` |
|
|
10
|
+
| `<Button variant="secondary">` | `<Button color="neutral">` |
|
|
11
|
+
| `<Button variant="critical">` | `<Button color="critical">` |
|
|
12
|
+
| `<GhostButton variant="primary">` | `<Button variant="ghost">` |
|
|
13
|
+
| `<GhostButton variant="secondary">` | `<Button variant="ghost" color="neutral">` |
|
|
14
|
+
| `<IconButton icon={<X />} />` | `<Button iconOnly><X /></Button>` |
|
|
15
|
+
| `<IconGhostButton icon={<X />} />` | `<Button variant="ghost" iconOnly><X /></Button>` |
|
|
16
|
+
| `<ButtonDeprecated …>` | `<Button …>` |
|
|
17
|
+
|
|
18
|
+
**Icon props are moved into children:**
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
// Before
|
|
22
|
+
<Button icon={<SaveIcon />} iconPosition="leading">Save</Button>
|
|
23
|
+
|
|
24
|
+
// After
|
|
25
|
+
<Button><SaveIcon />Save</Button>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
// Before
|
|
30
|
+
<GhostButton icon={<ShareIcon />} iconPosition="trailing">Share</GhostButton>
|
|
31
|
+
|
|
32
|
+
// After
|
|
33
|
+
<Button variant="ghost">Share<ShareIcon /></Button>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Imports are updated automatically:**
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// Before
|
|
40
|
+
import {Button, GhostButton, IconButton, IconGhostButton} from '@staffbase/design';
|
|
41
|
+
|
|
42
|
+
// After
|
|
43
|
+
import {Button} from '@staffbase/design';
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
### Step 1 — Install jscodeshift
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install --save-dev jscodeshift
|
|
52
|
+
# or
|
|
53
|
+
pnpm add -D jscodeshift
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Step 2 — Run the codemod
|
|
57
|
+
|
|
58
|
+
Point the `--transform` flag at this file inside your `node_modules`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx jscodeshift@latest \
|
|
62
|
+
--transform node_modules/@staffbase/design/codemods/v19-migrate-button/transform.js \
|
|
63
|
+
--extensions tsx,ts \
|
|
64
|
+
--parser tsx \
|
|
65
|
+
src/
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Run a **dry run** first to preview changes without writing files:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx jscodeshift@latest \
|
|
72
|
+
--transform node_modules/@staffbase/design/codemods/v19-migrate-button/transform.js \
|
|
73
|
+
--extensions tsx,ts \
|
|
74
|
+
--parser tsx \
|
|
75
|
+
--dry \
|
|
76
|
+
--print \
|
|
77
|
+
src/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Step 3 — Review and commit
|
|
81
|
+
|
|
82
|
+
1. Run your type-checker to catch anything the codemod couldn't handle statically:
|
|
83
|
+
```bash
|
|
84
|
+
npx tsc --noEmit
|
|
85
|
+
```
|
|
86
|
+
2. Run your tests.
|
|
87
|
+
3. Commit the result.
|
|
88
|
+
|
|
89
|
+
## What the codemod does NOT handle
|
|
90
|
+
|
|
91
|
+
- **Dynamic `variant` expressions** — e.g. `variant={someVariable}`. The original `variant` prop is preserved so TypeScript will surface a type error at that location. Update these manually.
|
|
92
|
+
- **Spread props** — e.g. `<Button {...buttonProps} />`. The spread is preserved; if `buttonProps` contains old-style variant/icon fields those object shapes need updating manually.
|
|
93
|
+
- **Type annotations** — `ButtonDeprecatedProps`, `GhostButtonProps`, etc. are removed from imports. If you use these types in your own interfaces or function signatures, TypeScript errors will point you to the right places.
|
|
94
|
+
- **Non-JSX prop passing** — e.g. `React.createElement(Button, {variant: 'secondary'})`. Only JSX syntax is transformed.
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jscodeshift codemod: Migrate deprecated Button components to the new unified Button.
|
|
3
|
+
*
|
|
4
|
+
* Transforms:
|
|
5
|
+
* Button (old) → Button (new API)
|
|
6
|
+
* ButtonDeprecated → Button (new API)
|
|
7
|
+
* GhostButton → Button variant="ghost"
|
|
8
|
+
* IconButton → Button iconOnly
|
|
9
|
+
* IconGhostButton → Button variant="ghost" iconOnly
|
|
10
|
+
*
|
|
11
|
+
* Old variant prop mapping:
|
|
12
|
+
* variant="primary" → (omit – default)
|
|
13
|
+
* variant="secondary" → color="neutral"
|
|
14
|
+
* variant="critical" → color="critical"
|
|
15
|
+
*
|
|
16
|
+
* Old icon/iconPosition props are moved into children:
|
|
17
|
+
* icon={<X />} iconPosition="leading" → <Button><X />text</Button>
|
|
18
|
+
* icon={<X />} iconPosition="trailing" → <Button>text<X /></Button>
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* npx jscodeshift@latest \
|
|
22
|
+
* --transform node_modules/@staffbase/design/codemods/v19-migrate-button/transform.js \
|
|
23
|
+
* --extensions tsx,ts \
|
|
24
|
+
* --parser tsx \
|
|
25
|
+
* src/
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const DESIGN_PKG = '@staffbase/design';
|
|
29
|
+
|
|
30
|
+
// Each deprecated component and how it maps to the new Button.
|
|
31
|
+
const DEPRECATED = {
|
|
32
|
+
/** Old solid Button. Only transformed when old-style props are detected. */
|
|
33
|
+
Button: {},
|
|
34
|
+
ButtonDeprecated: {},
|
|
35
|
+
GhostButton: {addVariant: 'ghost'},
|
|
36
|
+
IconButton: {addIconOnly: true, iconContentOnly: true},
|
|
37
|
+
IconGhostButton: {addVariant: 'ghost', addIconOnly: true, iconContentOnly: true},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Old variant value → new color value. null = default primary, omit the prop.
|
|
41
|
+
const VARIANT_TO_COLOR = {
|
|
42
|
+
primary: null,
|
|
43
|
+
secondary: 'neutral',
|
|
44
|
+
critical: 'critical',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Presence of any of these props on <Button> indicates v18 API usage.
|
|
48
|
+
const OLD_API_PROPS = new Set(['icon', 'iconPosition']);
|
|
49
|
+
const OLD_API_VARIANT_VALUES = new Set(['primary', 'secondary', 'critical']);
|
|
50
|
+
|
|
51
|
+
// ── Entry point ──────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export default function transform(file, api) {
|
|
54
|
+
const j = api.jscodeshift;
|
|
55
|
+
const root = j(file.source);
|
|
56
|
+
let dirty = false;
|
|
57
|
+
|
|
58
|
+
// 1. Find @staffbase/design import.
|
|
59
|
+
const designImports = root.find(j.ImportDeclaration, {source: {value: DESIGN_PKG}});
|
|
60
|
+
if (!designImports.length) return undefined;
|
|
61
|
+
|
|
62
|
+
// 2. Collect deprecated component imports: localName → { importedName, config }
|
|
63
|
+
const deprecated = new Map();
|
|
64
|
+
let buttonAlreadyImported = false;
|
|
65
|
+
|
|
66
|
+
designImports.find(j.ImportSpecifier).forEach((spec) => {
|
|
67
|
+
const importedName = spec.node.imported.name;
|
|
68
|
+
const localName = spec.node.local ? spec.node.local.name : importedName;
|
|
69
|
+
const config = DEPRECATED[importedName];
|
|
70
|
+
|
|
71
|
+
if (config !== undefined) {
|
|
72
|
+
deprecated.set(localName, {importedName, config});
|
|
73
|
+
} else if (importedName === 'Button') {
|
|
74
|
+
buttonAlreadyImported = true;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!deprecated.size) return undefined;
|
|
79
|
+
|
|
80
|
+
// 3. Transform JSX usages.
|
|
81
|
+
deprecated.forEach(({importedName, config}, localName) => {
|
|
82
|
+
root.find(j.JSXElement, {openingElement: {name: {name: localName}}}).forEach((path) => {
|
|
83
|
+
if (migrateElement(j, path, importedName, config)) dirty = true;
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!dirty) return undefined;
|
|
88
|
+
|
|
89
|
+
// 4. Update import specifiers.
|
|
90
|
+
// Remove deprecated ones (except old "Button" which maps to the new Button).
|
|
91
|
+
const toRemove = new Set();
|
|
92
|
+
deprecated.forEach(({importedName}) => {
|
|
93
|
+
if (importedName !== 'Button') toRemove.add(importedName);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (toRemove.size) {
|
|
97
|
+
designImports.find(j.ImportSpecifier).forEach((spec) => {
|
|
98
|
+
if (toRemove.has(spec.node.imported.name)) j(spec).remove();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Add Button specifier if none of the migrated components already provide it.
|
|
103
|
+
const buttonWasMigrated = deprecated.has('Button') || deprecated.has('ButtonDeprecated');
|
|
104
|
+
if (!buttonAlreadyImported && !buttonWasMigrated) {
|
|
105
|
+
designImports.get().node.specifiers.push(j.importSpecifier(j.identifier('Button')));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 5. Remove type imports for deprecated types (TypeScript projects).
|
|
109
|
+
const deprecatedTypes = new Set([
|
|
110
|
+
'ButtonDeprecatedProps',
|
|
111
|
+
'BaseButtonProps',
|
|
112
|
+
'ButtonVariant',
|
|
113
|
+
'ButtonIconPosition',
|
|
114
|
+
'GhostButtonProps',
|
|
115
|
+
'BaseGhostButtonProps',
|
|
116
|
+
'GhostButtonVariant',
|
|
117
|
+
'GhostButtonIconPosition',
|
|
118
|
+
'IconGhostButtonProps',
|
|
119
|
+
'IconButtonProps',
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
designImports.find(j.ImportSpecifier).forEach((spec) => {
|
|
123
|
+
if (deprecatedTypes.has(spec.node.imported.name)) j(spec).remove();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return root.toSource({quote: 'single'});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Element migration ────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function migrateElement(j, path, importedName, config) {
|
|
132
|
+
const {openingElement, closingElement} = path.node;
|
|
133
|
+
const existingAttrs = openingElement.attributes;
|
|
134
|
+
|
|
135
|
+
// For the old solid `Button`, only migrate if old-style props are present.
|
|
136
|
+
// This avoids touching any usages that are already on the new API.
|
|
137
|
+
if (importedName === 'Button') {
|
|
138
|
+
const hasOldStyleProps = existingAttrs.some((attr) => {
|
|
139
|
+
if (attr.type !== 'JSXAttribute') return false;
|
|
140
|
+
const name = attr.name.name;
|
|
141
|
+
if (OLD_API_PROPS.has(name)) return true;
|
|
142
|
+
if (name === 'variant') {
|
|
143
|
+
const val = getStringValue(attr.value);
|
|
144
|
+
return val !== null && OLD_API_VARIANT_VALUES.has(val);
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
});
|
|
148
|
+
if (!hasOldStyleProps) return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Index attributes by name.
|
|
152
|
+
const attrByName = new Map();
|
|
153
|
+
const spreadAttrs = [];
|
|
154
|
+
|
|
155
|
+
for (const attr of existingAttrs) {
|
|
156
|
+
if (attr.type === 'JSXSpreadAttribute') {
|
|
157
|
+
spreadAttrs.push(attr);
|
|
158
|
+
} else {
|
|
159
|
+
attrByName.set(attr.name.name, attr);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const variantAttr = attrByName.get('variant');
|
|
164
|
+
const iconAttr = attrByName.get('icon');
|
|
165
|
+
const iconPositionAttr = attrByName.get('iconPosition');
|
|
166
|
+
|
|
167
|
+
// Resolve new color prop.
|
|
168
|
+
let newColor = null; // null = use default primary, omit prop
|
|
169
|
+
let dynamicVariant = false;
|
|
170
|
+
|
|
171
|
+
if (variantAttr) {
|
|
172
|
+
const staticVal = getStringValue(variantAttr.value);
|
|
173
|
+
if (staticVal !== null && Object.prototype.hasOwnProperty.call(VARIANT_TO_COLOR, staticVal)) {
|
|
174
|
+
newColor = VARIANT_TO_COLOR[staticVal];
|
|
175
|
+
} else if (variantAttr.value && variantAttr.value.type === 'JSXExpressionContainer') {
|
|
176
|
+
// Dynamic variant — preserve the original attr and let TypeScript surface the error.
|
|
177
|
+
dynamicVariant = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Resolve icon placement.
|
|
182
|
+
const iconPosition =
|
|
183
|
+
getStringValue(iconPositionAttr ? iconPositionAttr.value : null) === 'trailing' ? 'trailing' : 'leading';
|
|
184
|
+
|
|
185
|
+
// Build new attribute list.
|
|
186
|
+
const newAttrs = [];
|
|
187
|
+
|
|
188
|
+
if (config.addVariant) {
|
|
189
|
+
newAttrs.push(j.jsxAttribute(j.jsxIdentifier('variant'), j.stringLiteral(config.addVariant)));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (newColor) {
|
|
193
|
+
newAttrs.push(j.jsxAttribute(j.jsxIdentifier('color'), j.stringLiteral(newColor)));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (config.addIconOnly) {
|
|
197
|
+
newAttrs.push(j.jsxAttribute(j.jsxIdentifier('iconOnly'), null));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Pass all other props through unchanged.
|
|
201
|
+
const DROP = new Set(['variant', 'icon', 'iconPosition']);
|
|
202
|
+
for (const [name, attr] of attrByName) {
|
|
203
|
+
if (!DROP.has(name)) newAttrs.push(attr);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Preserve spread attributes.
|
|
207
|
+
newAttrs.push(...spreadAttrs);
|
|
208
|
+
|
|
209
|
+
// Dynamic variant: keep the original attr so TS surfaces the error to the developer.
|
|
210
|
+
if (dynamicVariant && variantAttr) newAttrs.push(variantAttr);
|
|
211
|
+
|
|
212
|
+
// Build new children.
|
|
213
|
+
let newChildren = [...path.node.children];
|
|
214
|
+
|
|
215
|
+
if (iconAttr) {
|
|
216
|
+
const iconNode = unwrapIcon(j, iconAttr);
|
|
217
|
+
if (iconNode) {
|
|
218
|
+
if (config.iconContentOnly) {
|
|
219
|
+
newChildren = [iconNode];
|
|
220
|
+
} else if (iconPosition === 'trailing') {
|
|
221
|
+
newChildren = [...newChildren, iconNode];
|
|
222
|
+
} else {
|
|
223
|
+
newChildren = [iconNode, ...newChildren];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Apply changes.
|
|
229
|
+
openingElement.name.name = 'Button';
|
|
230
|
+
openingElement.attributes = newAttrs;
|
|
231
|
+
|
|
232
|
+
if (newChildren.length === 0) {
|
|
233
|
+
openingElement.selfClosing = true;
|
|
234
|
+
path.node.closingElement = null;
|
|
235
|
+
} else {
|
|
236
|
+
openingElement.selfClosing = false;
|
|
237
|
+
path.node.children = newChildren;
|
|
238
|
+
if (closingElement) {
|
|
239
|
+
closingElement.name.name = 'Button';
|
|
240
|
+
} else {
|
|
241
|
+
// Was self-closing (IconButton / IconGhostButton) — add closing tag.
|
|
242
|
+
path.node.closingElement = j.jsxClosingElement(j.jsxIdentifier('Button'));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
/** Returns the string value if node is a string literal, otherwise null. */
|
|
252
|
+
function getStringValue(node) {
|
|
253
|
+
if (!node) return null;
|
|
254
|
+
// variant="primary"
|
|
255
|
+
if (node.type === 'StringLiteral' || node.type === 'Literal') return node.value;
|
|
256
|
+
// variant={"primary"}
|
|
257
|
+
if (node.type === 'JSXExpressionContainer') {
|
|
258
|
+
const expr = node.expression;
|
|
259
|
+
if (expr.type === 'StringLiteral' || expr.type === 'Literal') return expr.value;
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Extracts the icon from an `icon={…}` JSX attribute as a JSX child node.
|
|
266
|
+
* Returns null if the value cannot be resolved.
|
|
267
|
+
*/
|
|
268
|
+
function unwrapIcon(j, iconAttr) {
|
|
269
|
+
const val = iconAttr.value;
|
|
270
|
+
if (!val || val.type !== 'JSXExpressionContainer') return null;
|
|
271
|
+
|
|
272
|
+
const expr = val.expression;
|
|
273
|
+
// icon={<SomeIcon />} — expression is a JSXElement, use it directly as child.
|
|
274
|
+
if (expr.type === 'JSXElement' || expr.type === 'JSXFragment') return expr;
|
|
275
|
+
// icon={someVariable} — wrap in expression container so it stays valid as a child.
|
|
276
|
+
return j.jsxExpressionContainer(expr);
|
|
277
|
+
}
|