@planningcenter/tapestry-migration-cli 3.4.1-rc.8 → 3.4.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 (32) hide show
  1. package/dist/tapestry-react-shim.cjs +34 -64
  2. package/package.json +3 -3
  3. package/src/components/dropdown/index.test.ts +177 -4
  4. package/src/components/dropdown/index.ts +24 -2
  5. package/src/components/dropdown/transforms/auditSpreadProps.test.ts +110 -0
  6. package/src/components/dropdown/transforms/auditSpreadProps.ts +10 -0
  7. package/src/components/dropdown/transforms/dividerToSeparator.test.ts +134 -0
  8. package/src/components/dropdown/transforms/dividerToSeparator.ts +67 -0
  9. package/src/components/dropdown/transforms/itemToAction.test.ts +19 -1
  10. package/src/components/dropdown/transforms/itemToAction.ts +14 -2
  11. package/src/components/dropdown/transforms/linkToLink.test.ts +19 -1
  12. package/src/components/dropdown/transforms/linkToLink.ts +13 -2
  13. package/src/components/dropdown/transforms/moveDropdownImport.test.ts +93 -0
  14. package/src/components/dropdown/transforms/moveDropdownImport.ts +13 -0
  15. package/src/components/dropdown/transforms/placementIdToMenu.test.ts +135 -0
  16. package/src/components/dropdown/transforms/placementIdToMenu.ts +96 -0
  17. package/src/components/dropdown/transforms/unsupportedProps.test.ts +145 -0
  18. package/src/components/dropdown/transforms/unsupportedProps.ts +12 -0
  19. package/src/components/dropdown/transforms/unsupportedPropsDivider.test.ts +143 -0
  20. package/src/components/dropdown/transforms/unsupportedPropsDivider.ts +26 -0
  21. package/src/components/dropdown/transforms/unsupportedPropsItem.test.ts +123 -0
  22. package/src/components/dropdown/transforms/unsupportedPropsItem.ts +12 -0
  23. package/src/components/dropdown/transforms/unsupportedPropsLink.test.ts +107 -0
  24. package/src/components/dropdown/transforms/unsupportedPropsLink.ts +12 -0
  25. package/src/components/dropdown/transforms/wrapMenu.test.ts +153 -0
  26. package/src/components/dropdown/transforms/wrapMenu.ts +54 -0
  27. package/src/components/dropdown/transforms/wrapTrigger.test.ts +283 -0
  28. package/src/components/dropdown/transforms/wrapTrigger.ts +98 -0
  29. package/src/components/input/transforms/unsupportedProps.test.ts +14 -13
  30. package/src/components/shared/conditions/isChildOf.test.ts +89 -0
  31. package/src/components/shared/conditions/isChildOf.ts +43 -0
  32. package/src/components/shared/helpers/unsupportedPropsHelpers.ts +36 -0
@@ -0,0 +1,283 @@
1
+ import jscodeshift from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import transform from "./wrapTrigger"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function applyTransform(source: string): string | null {
9
+ const fileInfo = { path: "test.tsx", source }
10
+ return transform(
11
+ fileInfo,
12
+ { j, jscodeshift: j, report: () => {}, stats: () => {} },
13
+ {}
14
+ ) as string | null
15
+ }
16
+
17
+ describe("wrapTrigger transform", () => {
18
+ describe("title prop", () => {
19
+ it("converts title string to DropdownTrigger + DropdownButton", () => {
20
+ const input = `
21
+ import { Dropdown } from "@planningcenter/tapestry-react"
22
+
23
+ export default function Test() {
24
+ return (
25
+ <Dropdown title="Actions">
26
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
27
+ </Dropdown>
28
+ )
29
+ }
30
+ `.trim()
31
+
32
+ const result = applyTransform(input)
33
+ expect(result).toContain("<DropdownTrigger>")
34
+ expect(result).toContain("</DropdownTrigger>")
35
+ expect(result).toContain('<DropdownButton label="Actions"')
36
+ expect(result).not.toContain('title="Actions"')
37
+ })
38
+
39
+ it("converts dynamic title expression to DropdownButton label", () => {
40
+ const input = `
41
+ import { Dropdown } from "@planningcenter/tapestry-react"
42
+
43
+ export default function Test() {
44
+ return (
45
+ <Dropdown title={menuTitle}>
46
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
47
+ </Dropdown>
48
+ )
49
+ }
50
+ `.trim()
51
+
52
+ const result = applyTransform(input)
53
+ expect(result).toContain("DropdownButton")
54
+ expect(result).toContain("label={menuTitle}")
55
+ expect(result).not.toContain("title=")
56
+ })
57
+
58
+ it("preserves existing Dropdown children after trigger insertion", () => {
59
+ const input = `
60
+ import { Dropdown } from "@planningcenter/tapestry-react"
61
+
62
+ export default function Test() {
63
+ return (
64
+ <Dropdown title="Actions">
65
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
66
+ <Dropdown.Item onSelect={fn2}>Delete</Dropdown.Item>
67
+ </Dropdown>
68
+ )
69
+ }
70
+ `.trim()
71
+
72
+ const result = applyTransform(input)
73
+ expect(result).toContain("<DropdownTrigger>")
74
+ expect(result).toContain("Dropdown.Item")
75
+ })
76
+ })
77
+
78
+ describe("triggerElement prop", () => {
79
+ it("wraps triggerElement JSX value in DropdownTrigger", () => {
80
+ const input = `
81
+ import { Dropdown } from "@planningcenter/tapestry-react"
82
+
83
+ export default function Test() {
84
+ return (
85
+ <Dropdown triggerElement={<DropdownButton label="Actions" />}>
86
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
87
+ </Dropdown>
88
+ )
89
+ }
90
+ `.trim()
91
+
92
+ const result = applyTransform(input)
93
+ expect(result).toContain("<DropdownTrigger>")
94
+ expect(result).toContain("</DropdownTrigger>")
95
+ expect(result).toContain("DropdownButton")
96
+ expect(result).not.toContain("triggerElement=")
97
+ })
98
+
99
+ it("triggerElement takes precedence over title", () => {
100
+ const input = `
101
+ import { Dropdown } from "@planningcenter/tapestry-react"
102
+
103
+ export default function Test() {
104
+ return (
105
+ <Dropdown title="Ignored" triggerElement={<DropdownButton label="Used" />}>
106
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
107
+ </Dropdown>
108
+ )
109
+ }
110
+ `.trim()
111
+
112
+ const result = applyTransform(input)
113
+ expect(result).toContain('label="Used"')
114
+ expect(result).not.toContain("title=")
115
+ expect(result).not.toContain("triggerElement=")
116
+ })
117
+ })
118
+
119
+ describe("attribute preservation", () => {
120
+ it("preserves other Dropdown props", () => {
121
+ const input = `
122
+ import { Dropdown } from "@planningcenter/tapestry-react"
123
+
124
+ export default function Test() {
125
+ return (
126
+ <Dropdown title="Actions" onClose={handleClose} placement="bottom-end">
127
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
128
+ </Dropdown>
129
+ )
130
+ }
131
+ `.trim()
132
+
133
+ const result = applyTransform(input)
134
+ expect(result).toContain("onClose={handleClose}")
135
+ expect(result).toContain('placement="bottom-end"')
136
+ })
137
+
138
+ it("does not touch the tapestry-react import statement", () => {
139
+ const input = `
140
+ import { Dropdown } from "@planningcenter/tapestry-react"
141
+
142
+ export default function Test() {
143
+ return (
144
+ <Dropdown title="Actions">
145
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
146
+ </Dropdown>
147
+ )
148
+ }
149
+ `.trim()
150
+
151
+ const result = applyTransform(input)
152
+ expect(result).toContain(
153
+ 'import { Dropdown } from "@planningcenter/tapestry-react"'
154
+ )
155
+ })
156
+
157
+ it("adds DropdownTrigger and DropdownButton imports from tapestry when title is used", () => {
158
+ const input = `
159
+ import { Dropdown } from "@planningcenter/tapestry-react"
160
+
161
+ export default function Test() {
162
+ return (
163
+ <Dropdown title="Actions">
164
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
165
+ </Dropdown>
166
+ )
167
+ }
168
+ `.trim()
169
+
170
+ const result = applyTransform(input)
171
+ expect(result).toContain('from "@planningcenter/tapestry"')
172
+ expect(result).toContain("DropdownTrigger")
173
+ expect(result).toContain("DropdownButton")
174
+ })
175
+
176
+ it("adds only DropdownTrigger import when triggerElement is provided", () => {
177
+ const input = `
178
+ import { Dropdown } from "@planningcenter/tapestry-react"
179
+
180
+ export default function Test() {
181
+ return (
182
+ <Dropdown triggerElement={<MyButton />}>
183
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
184
+ </Dropdown>
185
+ )
186
+ }
187
+ `.trim()
188
+
189
+ const result = applyTransform(input)
190
+ const tapestryImport =
191
+ result?.match(/import {[^}]+} from "@planningcenter\/tapestry"/)?.[0] ??
192
+ ""
193
+ expect(tapestryImport).toContain("DropdownTrigger")
194
+ expect(tapestryImport).not.toContain("DropdownButton")
195
+ })
196
+ })
197
+
198
+ describe("multiple dropdowns", () => {
199
+ it("transforms all Dropdown elements in the file", () => {
200
+ const input = `
201
+ import { Dropdown } from "@planningcenter/tapestry-react"
202
+
203
+ export default function Test() {
204
+ return (
205
+ <div>
206
+ <Dropdown title="First">
207
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
208
+ </Dropdown>
209
+ <Dropdown title="Second">
210
+ <Dropdown.Item onSelect={fn2}>Delete</Dropdown.Item>
211
+ </Dropdown>
212
+ </div>
213
+ )
214
+ }
215
+ `.trim()
216
+
217
+ const result = applyTransform(input)
218
+ const count = (result?.match(/<DropdownTrigger>/g) || []).length
219
+ expect(count).toBe(2)
220
+ })
221
+ })
222
+
223
+ describe("no changes scenarios", () => {
224
+ it("returns null when Dropdown has no title or triggerElement", () => {
225
+ const input = `
226
+ import { Dropdown } from "@planningcenter/tapestry-react"
227
+
228
+ export default function Test() {
229
+ return (
230
+ <Dropdown onClose={handleClose}>
231
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
232
+ </Dropdown>
233
+ )
234
+ }
235
+ `.trim()
236
+
237
+ const result = applyTransform(input)
238
+ expect(result).toBe(null)
239
+ })
240
+
241
+ it("returns null when Dropdown is not imported from tapestry-react", () => {
242
+ const input = `
243
+ import { Dropdown } from "some-other-library"
244
+
245
+ export default function Test() {
246
+ return (
247
+ <Dropdown title="Actions">
248
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
249
+ </Dropdown>
250
+ )
251
+ }
252
+ `.trim()
253
+
254
+ const result = applyTransform(input)
255
+ expect(result).toBe(null)
256
+ })
257
+
258
+ it("returns null for empty file", () => {
259
+ expect(applyTransform("")).toBe(null)
260
+ })
261
+
262
+ it("wraps non-JSX triggerElement expression as a child with a TODO comment", () => {
263
+ const input = `
264
+ import { Dropdown } from "@planningcenter/tapestry-react"
265
+
266
+ export default function Test() {
267
+ return (
268
+ <Dropdown triggerElement={myTrigger}>
269
+ <Dropdown.Item onSelect={fn}>Edit</Dropdown.Item>
270
+ </Dropdown>
271
+ )
272
+ }
273
+ `.trim()
274
+
275
+ const result = applyTransform(input)
276
+ expect(result).toContain("<DropdownTrigger>")
277
+ expect(result).toContain("{myTrigger}")
278
+ expect(result).not.toContain("triggerElement=")
279
+ expect(result).toContain("TODO")
280
+ expect(result).toContain("triggerElement")
281
+ })
282
+ })
283
+ })
@@ -0,0 +1,98 @@
1
+ import {
2
+ JSXAttribute,
3
+ JSXElement,
4
+ JSXExpressionContainer,
5
+ Transform,
6
+ } from "jscodeshift"
7
+
8
+ import { addComment } from "../../shared/actions/addComment"
9
+ import { getAttribute } from "../../shared/actions/getAttribute"
10
+ import { removeAttribute } from "../../shared/actions/removeAttribute"
11
+ import { attributeTransformFactory } from "../../shared/transformFactories/attributeTransformFactory"
12
+ import { addImport } from "../../shared/transformFactories/helpers/manageImports"
13
+
14
+ const transform: Transform = attributeTransformFactory({
15
+ targetComponent: "Dropdown",
16
+ targetPackage: "@planningcenter/tapestry-react",
17
+ transform: (element, { j, source }) => {
18
+ const titleAttr = getAttribute({ element, name: "title" })
19
+ const triggerElementAttr = getAttribute({ element, name: "triggerElement" })
20
+
21
+ if (!titleAttr && !triggerElementAttr) return false
22
+
23
+ let triggerChild: JSXElement | JSXExpressionContainer | null = null
24
+
25
+ if (triggerElementAttr) {
26
+ const attrValue = triggerElementAttr.value
27
+ if (
28
+ attrValue?.type === "JSXExpressionContainer" &&
29
+ attrValue.expression.type !== "JSXEmptyExpression"
30
+ ) {
31
+ if (attrValue.expression.type === "JSXElement") {
32
+ triggerChild = attrValue.expression
33
+ } else {
34
+ triggerChild = j.jsxExpressionContainer(attrValue.expression)
35
+ addComment({
36
+ element,
37
+ j,
38
+ scope: "triggerElement",
39
+ source,
40
+ text: "triggerElement was not a JSX element — verify this renders correctly inside DropdownTrigger",
41
+ })
42
+ }
43
+ }
44
+ }
45
+
46
+ if (!triggerChild && titleAttr) {
47
+ const labelValue = titleAttr.value
48
+ if (!labelValue) return false
49
+
50
+ const labelAttr: JSXAttribute = j.jsxAttribute(
51
+ j.jsxIdentifier("label"),
52
+ labelValue
53
+ )
54
+
55
+ triggerChild = j.jsxElement(
56
+ j.jsxOpeningElement(
57
+ j.jsxIdentifier("DropdownButton"),
58
+ [labelAttr],
59
+ true
60
+ ),
61
+ null,
62
+ []
63
+ )
64
+ addImport({
65
+ component: "DropdownButton",
66
+ fromPackage: "@planningcenter/tapestry-react",
67
+ j,
68
+ pkg: "@planningcenter/tapestry",
69
+ source,
70
+ })
71
+ }
72
+
73
+ if (!triggerChild) return false
74
+
75
+ const triggerWrapper = j.jsxElement(
76
+ j.jsxOpeningElement(j.jsxIdentifier("DropdownTrigger"), [], false),
77
+ j.jsxClosingElement(j.jsxIdentifier("DropdownTrigger")),
78
+ [triggerChild]
79
+ )
80
+
81
+ removeAttribute("title", { element, j, source })
82
+ removeAttribute("triggerElement", { element, j, source })
83
+
84
+ element.children = [triggerWrapper, ...(element.children ?? [])]
85
+
86
+ addImport({
87
+ component: "DropdownTrigger",
88
+ fromPackage: "@planningcenter/tapestry-react",
89
+ j,
90
+ pkg: "@planningcenter/tapestry",
91
+ source,
92
+ })
93
+
94
+ return true
95
+ },
96
+ })
97
+
98
+ export default transform
@@ -156,19 +156,6 @@ function Test() {
156
156
  expect(result).toContain("TODO: tapestry-migration (hover)")
157
157
  })
158
158
 
159
- it("should flag autoFocus", () => {
160
- const input = `
161
- import { Input } from "@planningcenter/tapestry-react"
162
-
163
- function Test() {
164
- return <Input autoFocus label="Name" />
165
- }
166
- `.trim()
167
-
168
- const result = applyTransform(input)
169
- expect(result).toContain("TODO: tapestry-migration (autoFocus)")
170
- })
171
-
172
159
  it("should flag readOnlyBackgroundColor", () => {
173
160
  const input = `
174
161
  import { Input } from "@planningcenter/tapestry-react"
@@ -231,6 +218,20 @@ function Test() {
231
218
  expect(result).not.toContain("TODO: tapestry-migration")
232
219
  })
233
220
 
221
+ it("should not flag autoFocus", () => {
222
+ const input = `
223
+ import { Input } from "@planningcenter/tapestry-react"
224
+
225
+ function Test() {
226
+ return <Input autoFocus label="Name" />
227
+ }
228
+ `.trim()
229
+
230
+ const result = applyTransform(input)
231
+ expect(result).not.toContain("TODO: tapestry-migration")
232
+ expect(result).toContain("autoFocus")
233
+ })
234
+
234
235
  it("should not flag standard HTML attributes", () => {
235
236
  const input = `
236
237
  import { Input } from "@planningcenter/tapestry-react"
@@ -0,0 +1,89 @@
1
+ import jscodeshift, { JSXElement } from "jscodeshift"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import { isChildOf } from "./isChildOf"
5
+
6
+ const j = jscodeshift.withParser("tsx")
7
+
8
+ function getElements(source: string): {
9
+ elements: JSXElement[]
10
+ source: ReturnType<typeof j>
11
+ } {
12
+ const parsed = j(source)
13
+ const elements = parsed
14
+ .find(j.JSXElement)
15
+ .paths()
16
+ .map((p) => p.value)
17
+ return { elements, source: parsed }
18
+ }
19
+
20
+ describe("isChildOf", () => {
21
+ it("returns true for a direct child of the parent", () => {
22
+ const { source, elements } = getElements(`<Dropdown><Divider /></Dropdown>`)
23
+ const divider = elements.find(
24
+ (el) =>
25
+ el.openingElement.name.type === "JSXIdentifier" &&
26
+ el.openingElement.name.name === "Divider"
27
+ )!
28
+ const condition = isChildOf("Dropdown", source, j)
29
+ expect(condition(divider)).toBe(true)
30
+ })
31
+
32
+ it("returns true for a deeply nested descendant", () => {
33
+ const { source, elements } = getElements(
34
+ `<Dropdown><DropdownMenu><Divider /></DropdownMenu></Dropdown>`
35
+ )
36
+ const divider = elements.find(
37
+ (el) =>
38
+ el.openingElement.name.type === "JSXIdentifier" &&
39
+ el.openingElement.name.name === "Divider"
40
+ )!
41
+ const condition = isChildOf("Dropdown", source, j)
42
+ expect(condition(divider)).toBe(true)
43
+ })
44
+
45
+ it("returns false for an element outside the parent", () => {
46
+ const { source, elements } = getElements(`<div><Divider /></div>`)
47
+ const divider = elements.find(
48
+ (el) =>
49
+ el.openingElement.name.type === "JSXIdentifier" &&
50
+ el.openingElement.name.name === "Divider"
51
+ )!
52
+ const condition = isChildOf("Dropdown", source, j)
53
+ expect(condition(divider)).toBe(false)
54
+ })
55
+
56
+ it("returns false when no parent element exists in source", () => {
57
+ const { source, elements } = getElements(`<Divider />`)
58
+ const divider = elements[0]
59
+ const condition = isChildOf("Dropdown", source, j)
60
+ expect(condition(divider)).toBe(false)
61
+ })
62
+
63
+ it("returns true for child inside one of multiple parents", () => {
64
+ const { source, elements } = getElements(
65
+ `<div><Dropdown><Divider /></Dropdown></div>`
66
+ )
67
+ const divider = elements.find(
68
+ (el) =>
69
+ el.openingElement.name.type === "JSXIdentifier" &&
70
+ el.openingElement.name.name === "Divider"
71
+ )!
72
+ const condition = isChildOf("Dropdown", source, j)
73
+ expect(condition(divider)).toBe(true)
74
+ })
75
+
76
+ it("handles sibling elements — child inside parent returns true, sibling does not", () => {
77
+ const { source, elements } = getElements(
78
+ `<div><Dropdown><Divider /></Dropdown><Divider /></div>`
79
+ )
80
+ const [insideDivider, outsideDivider] = elements.filter(
81
+ (el) =>
82
+ el.openingElement.name.type === "JSXIdentifier" &&
83
+ el.openingElement.name.name === "Divider"
84
+ )
85
+ const condition = isChildOf("Dropdown", source, j)
86
+ expect(condition(insideDivider)).toBe(true)
87
+ expect(condition(outsideDivider)).toBe(false)
88
+ })
89
+ })
@@ -0,0 +1,43 @@
1
+ import { Collection, JSCodeshift, JSXElement } from "jscodeshift"
2
+
3
+ import { TransformCondition } from "../types"
4
+
5
+ function elementKey(element: JSXElement): string | null {
6
+ const loc = element.loc
7
+ if (!loc) return null
8
+ return `${loc.start.line}:${loc.start.column}-${loc.end.line}:${loc.end.column}`
9
+ }
10
+
11
+ function collectDescendantKeys(element: JSXElement, result: Set<string>): void {
12
+ for (const child of element.children ?? []) {
13
+ if (child.type === "JSXElement") {
14
+ const key = elementKey(child)
15
+ if (key) result.add(key)
16
+ collectDescendantKeys(child, result)
17
+ }
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Returns a condition that is true only when the element is a descendant of a
23
+ * JSX element with the given local name. Matches by source location so the
24
+ * condition works across separate jscodeshift parses of the same source text.
25
+ */
26
+ export function isChildOf(
27
+ parentLocalName: string,
28
+ source: Collection,
29
+ j: JSCodeshift
30
+ ): TransformCondition {
31
+ const descendantKeys = new Set<string>()
32
+
33
+ source
34
+ .find(j.JSXOpeningElement, { name: { name: parentLocalName } })
35
+ .forEach((path) => {
36
+ collectDescendantKeys(path.parent.value as JSXElement, descendantKeys)
37
+ })
38
+
39
+ return (element: JSXElement) => {
40
+ const key = elementKey(element)
41
+ return key !== null && descendantKeys.has(key)
42
+ }
43
+ }
@@ -56,6 +56,7 @@ export const CHECKBOX_RADIO_SUPPORTED_PROPS = [
56
56
 
57
57
  export const INPUT_SPECIFIC_PROPS = [
58
58
  "autoComplete",
59
+ "autoFocus",
59
60
  "autoWidth",
60
61
  "defaultValue",
61
62
  "description",
@@ -197,3 +198,38 @@ export const DATE_PICKER_SUPPORTED_PROPS = [
197
198
  ...COMMON_PROPS,
198
199
  ...DATE_PICKER_SPECIFIC_PROPS,
199
200
  ]
201
+
202
+ export const DROPDOWN_SPECIFIC_PROPS = ["onClose", "onOpen"]
203
+
204
+ export const DROPDOWN_SUPPORTED_PROPS = [
205
+ ...COMMON_PROPS.filter((prop) => prop !== "size"),
206
+ ...DROPDOWN_SPECIFIC_PROPS,
207
+ ]
208
+
209
+ export const DROPDOWN_ITEM_BASE_PROPS = [
210
+ "destructive",
211
+ "staffOnly",
212
+ "text",
213
+ "textValue",
214
+ "value",
215
+ ]
216
+
217
+ // Dropdown.Item (old) → DropdownAction (new)
218
+ // onSelect→onAction, value→id, text→textValue handled by itemToAction
219
+ export const DROPDOWN_ITEM_SUPPORTED_PROPS = [
220
+ ...COMMON_PROPS,
221
+ ...DROPDOWN_ITEM_BASE_PROPS,
222
+ "disabled",
223
+ "onAction",
224
+ "onSelect",
225
+ ]
226
+
227
+ // Dropdown.Link (old) → DropdownLink (new)
228
+ // to→href handled by linkToLink; disabled intentionally NOT included
229
+ export const DROPDOWN_LINK_SUPPORTED_PROPS = [
230
+ ...COMMON_PROPS,
231
+ ...DROPDOWN_ITEM_BASE_PROPS,
232
+ "external",
233
+ "href",
234
+ "to",
235
+ ]