@trackunit/react-components 1.24.7 → 1.25.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 (78) hide show
  1. package/index.cjs.js +2298 -2199
  2. package/index.esm.js +2299 -2200
  3. package/migrations/entry.js +3 -0
  4. package/migrations/entry.js.map +1 -0
  5. package/migrations/utils/jsx-utils.js +203 -0
  6. package/migrations/utils/jsx-utils.js.map +1 -0
  7. package/migrations/v2-0-0/breadcrumb-remove-deprecated-props.js +67 -0
  8. package/migrations/v2-0-0/breadcrumb-remove-deprecated-props.js.map +1 -0
  9. package/migrations/v2-0-0/cardheader-onclickclose-to-actions.js +101 -0
  10. package/migrations/v2-0-0/cardheader-onclickclose-to-actions.js.map +1 -0
  11. package/migrations/v2-0-0/iconbutton-add-required-name.js +59 -0
  12. package/migrations/v2-0-0/iconbutton-add-required-name.js.map +1 -0
  13. package/migrations/v2-0-0/kpi-tooltiplabel-to-wrapper.js +95 -0
  14. package/migrations/v2-0-0/kpi-tooltiplabel-to-wrapper.js.map +1 -0
  15. package/migrations/v2-0-0/notice-tooltiplabel-to-wrapper.js +114 -0
  16. package/migrations/v2-0-0/notice-tooltiplabel-to-wrapper.js.map +1 -0
  17. package/migrations/v2-0-0/pagination-add-required-titles.js +65 -0
  18. package/migrations/v2-0-0/pagination-add-required-titles.js.map +1 -0
  19. package/migrations/v2-0-0/tag-add-removetaglabel.js +59 -0
  20. package/migrations/v2-0-0/tag-add-removetaglabel.js.map +1 -0
  21. package/migrations/v2-0-0/togglegroup-remove-item-style.js +100 -0
  22. package/migrations/v2-0-0/togglegroup-remove-item-style.js.map +1 -0
  23. package/migrations.json +44 -0
  24. package/package.json +7 -6
  25. package/src/components/Breadcrumb/BreadcrumbForMediumScreen.d.ts +2 -2
  26. package/src/components/Breadcrumb/utils/types.d.ts +1 -0
  27. package/src/components/Card/Card.variants.d.ts +4 -4
  28. package/src/components/Card/CardHeader.d.ts +3 -8
  29. package/src/components/Icon/Icon.variants.d.ts +1 -1
  30. package/src/components/InteractableItem/InteractableItem.variants.d.ts +1 -1
  31. package/src/components/KPI/KPI.d.ts +1 -5
  32. package/src/components/KPI/KPI.variants.d.ts +1 -1
  33. package/src/components/KPICard/KPICard.d.ts +5 -1
  34. package/src/components/Menu/MoreMenu/MoreMenu.d.ts +8 -3
  35. package/src/components/Notice/Notice.d.ts +1 -9
  36. package/src/components/Pagination/Pagination.d.ts +11 -1
  37. package/src/components/Tag/Tag.d.ts +35 -13
  38. package/src/components/Tag/Tag.variants.d.ts +1 -1
  39. package/src/components/Text/Text.variants.d.ts +1 -1
  40. package/src/components/ToggleGroup/ToggleGroup.d.ts +81 -36
  41. package/src/components/buttons/Button/Button.d.ts +1 -1
  42. package/src/components/buttons/IconButton/IconButton.d.ts +28 -6
  43. package/src/components/buttons/shared/ButtonProps.d.ts +3 -2
  44. package/src/translation.d.ts +30 -0
  45. package/translation.cjs.js +7 -0
  46. package/translation.cjs10.js +7 -0
  47. package/translation.cjs11.js +7 -0
  48. package/translation.cjs12.js +7 -0
  49. package/translation.cjs13.js +7 -0
  50. package/translation.cjs14.js +7 -0
  51. package/translation.cjs15.js +7 -0
  52. package/translation.cjs16.js +7 -0
  53. package/translation.cjs17.js +7 -0
  54. package/translation.cjs2.js +7 -0
  55. package/translation.cjs3.js +7 -0
  56. package/translation.cjs4.js +7 -0
  57. package/translation.cjs5.js +7 -0
  58. package/translation.cjs6.js +7 -0
  59. package/translation.cjs7.js +7 -0
  60. package/translation.cjs8.js +7 -0
  61. package/translation.cjs9.js +7 -0
  62. package/translation.esm.js +5 -0
  63. package/translation.esm10.js +5 -0
  64. package/translation.esm11.js +5 -0
  65. package/translation.esm12.js +5 -0
  66. package/translation.esm13.js +5 -0
  67. package/translation.esm14.js +5 -0
  68. package/translation.esm15.js +5 -0
  69. package/translation.esm16.js +5 -0
  70. package/translation.esm17.js +5 -0
  71. package/translation.esm2.js +5 -0
  72. package/translation.esm3.js +5 -0
  73. package/translation.esm4.js +5 -0
  74. package/translation.esm5.js +5 -0
  75. package/translation.esm6.js +5 -0
  76. package/translation.esm7.js +5 -0
  77. package/translation.esm8.js +5 -0
  78. package/translation.esm9.js +5 -0
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/react/components/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["// Migration entry point for @nx/js:tsc build target.\n// Migrations are registered in ../migrations.json and resolved by NX at runtime.\nexport {};\n"]}
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.insertAttributeIntoOpeningTag = exports.logSummary = exports.tsquery = exports.parseTsx = exports.hasSpreadAttribute = exports.findJsxAttribute = exports.findJsxElements = exports.getLocalAliasFor = exports.getImportedAliases = exports.visitTsFiles = exports.visitTsxFiles = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const devkit_1 = require("@nx/devkit");
6
+ const tsquery_1 = require("@phenomnomnominal/tsquery");
7
+ Object.defineProperty(exports, "tsquery", { enumerable: true, get: function () { return tsquery_1.tsquery; } });
8
+ const ts = tslib_1.__importStar(require("typescript"));
9
+ const TSX_EXTENSIONS = [".tsx", ".jsx"];
10
+ const TS_EXTENSIONS = [".ts", ".tsx", ".jsx"];
11
+ const isTsxFile = (filePath) => TSX_EXTENSIONS.some(ext => filePath.endsWith(ext));
12
+ const isTsFile = (filePath) => TS_EXTENSIONS.some(ext => filePath.endsWith(ext));
13
+ const isUnderNodeModules = (filePath) => filePath.includes("/node_modules/") || filePath.startsWith("node_modules/");
14
+ const isUnderDist = (filePath) => filePath.includes("/dist/") || filePath.startsWith("dist/") || filePath.includes("/.nx/");
15
+ /**
16
+ * Visits every `.tsx`/`.jsx` file under the workspace root and invokes the
17
+ * callback with the file path and its current contents. Files that don't
18
+ * include the marker substring are skipped, which makes large workspaces
19
+ * cheap to scan.
20
+ */
21
+ const visitTsxFiles = (tree, marker, callback) => {
22
+ let touched = 0;
23
+ (0, devkit_1.visitNotIgnoredFiles)(tree, "/", filePath => {
24
+ if (!isTsxFile(filePath))
25
+ return;
26
+ if (isUnderNodeModules(filePath) || isUnderDist(filePath))
27
+ return;
28
+ const content = tree.read(filePath, "utf-8");
29
+ if (content === null)
30
+ return;
31
+ if (!content.includes(marker))
32
+ return;
33
+ const updated = callback(filePath, content);
34
+ if (updated !== null && updated !== undefined && updated !== content) {
35
+ tree.write(filePath, updated);
36
+ touched += 1;
37
+ }
38
+ });
39
+ return touched;
40
+ };
41
+ exports.visitTsxFiles = visitTsxFiles;
42
+ /**
43
+ * Same as {@link visitTsxFiles} but also includes plain `.ts` files. Use this
44
+ * when the codemod also rewrites non-JSX files such as helpers or hooks.
45
+ */
46
+ const visitTsFiles = (tree, marker, callback) => {
47
+ let touched = 0;
48
+ (0, devkit_1.visitNotIgnoredFiles)(tree, "/", filePath => {
49
+ if (!isTsFile(filePath))
50
+ return;
51
+ if (isUnderNodeModules(filePath) || isUnderDist(filePath))
52
+ return;
53
+ const content = tree.read(filePath, "utf-8");
54
+ if (content === null)
55
+ return;
56
+ if (!content.includes(marker))
57
+ return;
58
+ const updated = callback(filePath, content);
59
+ if (updated !== null && updated !== undefined && updated !== content) {
60
+ tree.write(filePath, updated);
61
+ touched += 1;
62
+ }
63
+ });
64
+ return touched;
65
+ };
66
+ exports.visitTsFiles = visitTsFiles;
67
+ /**
68
+ * Mapping of imported component aliases to their original names for a given
69
+ * package. For `import { IconButton as Btn } from "@trackunit/react-components"`
70
+ * this returns `{ Btn: "IconButton" }`.
71
+ *
72
+ * Returns `null` when the file imports nothing from the package, which lets
73
+ * callers short-circuit before parsing JSX.
74
+ */
75
+ const getImportedAliases = (sourceFile, packageName) => {
76
+ const result = {};
77
+ let found = false;
78
+ for (const stmt of sourceFile.statements) {
79
+ if (!ts.isImportDeclaration(stmt))
80
+ continue;
81
+ const moduleSpecifier = stmt.moduleSpecifier;
82
+ if (!ts.isStringLiteral(moduleSpecifier))
83
+ continue;
84
+ if (moduleSpecifier.text !== packageName)
85
+ continue;
86
+ const namedBindings = stmt.importClause?.namedBindings;
87
+ if (namedBindings === undefined || !ts.isNamedImports(namedBindings))
88
+ continue;
89
+ for (const element of namedBindings.elements) {
90
+ const localName = element.name.text;
91
+ const importedName = element.propertyName?.text ?? localName;
92
+ result[localName] = importedName;
93
+ found = true;
94
+ }
95
+ }
96
+ return found ? result : null;
97
+ };
98
+ exports.getImportedAliases = getImportedAliases;
99
+ /**
100
+ * Returns the local alias used in this file for `originalName` when imported
101
+ * from `packageName`, or `null` if the component is not imported.
102
+ */
103
+ const getLocalAliasFor = (sourceFile, packageName, originalName) => {
104
+ const aliases = (0, exports.getImportedAliases)(sourceFile, packageName);
105
+ if (aliases === null)
106
+ return null;
107
+ for (const [local, original] of Object.entries(aliases)) {
108
+ if (original === originalName)
109
+ return local;
110
+ }
111
+ return null;
112
+ };
113
+ exports.getLocalAliasFor = getLocalAliasFor;
114
+ /**
115
+ * Finds every JSX element whose tag name resolves to one of `tagNames`
116
+ * (typically the local aliases returned by {@link getImportedAliases}).
117
+ *
118
+ * The result intentionally includes both opening and full elements so callers
119
+ * can manipulate attributes (via `openingElement`) or wrap/replace the whole
120
+ * element (via `element`).
121
+ */
122
+ const findJsxElements = (sourceFile, tagNames) => {
123
+ const matches = [];
124
+ const tagSet = new Set(tagNames);
125
+ const visit = (node) => {
126
+ if (ts.isJsxSelfClosingElement(node)) {
127
+ if (ts.isIdentifier(node.tagName) && tagSet.has(node.tagName.text)) {
128
+ matches.push({ openingElement: node, element: node });
129
+ }
130
+ }
131
+ else if (ts.isJsxElement(node)) {
132
+ const opening = node.openingElement;
133
+ if (ts.isIdentifier(opening.tagName) && tagSet.has(opening.tagName.text)) {
134
+ matches.push({ openingElement: opening, element: node });
135
+ }
136
+ }
137
+ ts.forEachChild(node, visit);
138
+ };
139
+ visit(sourceFile);
140
+ return matches;
141
+ };
142
+ exports.findJsxElements = findJsxElements;
143
+ /**
144
+ * Looks up a named JSX attribute on the opening element. Returns `null` when
145
+ * the attribute is missing or part of a spread attribute (which we cannot
146
+ * statically inspect).
147
+ */
148
+ const findJsxAttribute = (openingElement, attributeName) => {
149
+ for (const attr of openingElement.attributes.properties) {
150
+ if (!ts.isJsxAttribute(attr))
151
+ continue;
152
+ if (!ts.isIdentifier(attr.name))
153
+ continue;
154
+ if (attr.name.text === attributeName)
155
+ return attr;
156
+ }
157
+ return null;
158
+ };
159
+ exports.findJsxAttribute = findJsxAttribute;
160
+ /**
161
+ * `true` when the element forwards a spread expression such as `{...rest}`,
162
+ * which means we can't be sure which attributes are actually set.
163
+ */
164
+ const hasSpreadAttribute = (openingElement) => {
165
+ return openingElement.attributes.properties.some(prop => ts.isJsxSpreadAttribute(prop));
166
+ };
167
+ exports.hasSpreadAttribute = hasSpreadAttribute;
168
+ /**
169
+ * Parses the file as TSX so JSX is recognised. We do not need a full program
170
+ * here; tsquery's `ast` helper produces a script-kind source file which loses
171
+ * JSX context, so we go through `createSourceFile` directly.
172
+ */
173
+ const parseTsx = (content, fileName = "file.tsx") => ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
174
+ exports.parseTsx = parseTsx;
175
+ /**
176
+ * Helper for codemods to log a one-line summary at the end of a run. Migration
177
+ * runners aggregate logs per migration, so this keeps output focused.
178
+ */
179
+ const logSummary = (migrationName, touched, manualReviewNeeded) => {
180
+ if (touched === 0) {
181
+ devkit_1.logger.info(` ${migrationName}: no files changed`);
182
+ return;
183
+ }
184
+ const reviewSuffix = manualReviewNeeded !== undefined && manualReviewNeeded > 0
185
+ ? ` (${manualReviewNeeded} location(s) need manual review)`
186
+ : "";
187
+ devkit_1.logger.info(` ${migrationName}: updated ${touched} file(s)${reviewSuffix}`);
188
+ };
189
+ exports.logSummary = logSummary;
190
+ /**
191
+ * Inserts an attribute string immediately after the tag name (so the new
192
+ * attribute appears first). Returns the resulting source content.
193
+ *
194
+ * The added attribute is rendered verbatim, so callers are responsible for
195
+ * including the leading space (e.g. ` title="Close"`).
196
+ */
197
+ const insertAttributeIntoOpeningTag = (content, openingElement, attributeText) => {
198
+ const tagName = openingElement.tagName;
199
+ const insertPos = tagName.getEnd();
200
+ return content.slice(0, insertPos) + ` ${attributeText}` + content.slice(insertPos);
201
+ };
202
+ exports.insertAttributeIntoOpeningTag = insertAttributeIntoOpeningTag;
203
+ //# sourceMappingURL=jsx-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsx-utils.js","sourceRoot":"","sources":["../../../../../../libs/react/components/migrations/utils/jsx-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { logger, type Tree, visitNotIgnoredFiles } from \"@nx/devkit\";\nimport { tsquery } from \"@phenomnomnominal/tsquery\";\nimport * as ts from \"typescript\";\n\nconst TSX_EXTENSIONS = [\".tsx\", \".jsx\"] as const;\nconst TS_EXTENSIONS = [\".ts\", \".tsx\", \".jsx\"] as const;\n\nconst isTsxFile = (filePath: string): boolean => TSX_EXTENSIONS.some(ext => filePath.endsWith(ext));\nconst isTsFile = (filePath: string): boolean => TS_EXTENSIONS.some(ext => filePath.endsWith(ext));\n\nconst isUnderNodeModules = (filePath: string): boolean =>\n filePath.includes(\"/node_modules/\") || filePath.startsWith(\"node_modules/\");\n\nconst isUnderDist = (filePath: string): boolean =>\n filePath.includes(\"/dist/\") || filePath.startsWith(\"dist/\") || filePath.includes(\"/.nx/\");\n\n/**\n * Visits every `.tsx`/`.jsx` file under the workspace root and invokes the\n * callback with the file path and its current contents. Files that don't\n * include the marker substring are skipped, which makes large workspaces\n * cheap to scan.\n */\nexport const visitTsxFiles = (\n tree: Tree,\n marker: string,\n callback: (filePath: string, content: string) => string | null | undefined\n): number => {\n let touched = 0;\n visitNotIgnoredFiles(tree, \"/\", filePath => {\n if (!isTsxFile(filePath)) return;\n if (isUnderNodeModules(filePath) || isUnderDist(filePath)) return;\n const content = tree.read(filePath, \"utf-8\");\n if (content === null) return;\n if (!content.includes(marker)) return;\n const updated = callback(filePath, content);\n if (updated !== null && updated !== undefined && updated !== content) {\n tree.write(filePath, updated);\n touched += 1;\n }\n });\n return touched;\n};\n\n/**\n * Same as {@link visitTsxFiles} but also includes plain `.ts` files. Use this\n * when the codemod also rewrites non-JSX files such as helpers or hooks.\n */\nexport const visitTsFiles = (\n tree: Tree,\n marker: string,\n callback: (filePath: string, content: string) => string | null | undefined\n): number => {\n let touched = 0;\n visitNotIgnoredFiles(tree, \"/\", filePath => {\n if (!isTsFile(filePath)) return;\n if (isUnderNodeModules(filePath) || isUnderDist(filePath)) return;\n const content = tree.read(filePath, \"utf-8\");\n if (content === null) return;\n if (!content.includes(marker)) return;\n const updated = callback(filePath, content);\n if (updated !== null && updated !== undefined && updated !== content) {\n tree.write(filePath, updated);\n touched += 1;\n }\n });\n return touched;\n};\n\n/**\n * Mapping of imported component aliases to their original names for a given\n * package. For `import { IconButton as Btn } from \"@trackunit/react-components\"`\n * this returns `{ Btn: \"IconButton\" }`.\n *\n * Returns `null` when the file imports nothing from the package, which lets\n * callers short-circuit before parsing JSX.\n */\nexport const getImportedAliases = (sourceFile: ts.SourceFile, packageName: string): Record<string, string> | null => {\n const result: Record<string, string> = {};\n let found = false;\n\n for (const stmt of sourceFile.statements) {\n if (!ts.isImportDeclaration(stmt)) continue;\n const moduleSpecifier = stmt.moduleSpecifier;\n if (!ts.isStringLiteral(moduleSpecifier)) continue;\n if (moduleSpecifier.text !== packageName) continue;\n\n const namedBindings = stmt.importClause?.namedBindings;\n if (namedBindings === undefined || !ts.isNamedImports(namedBindings)) continue;\n\n for (const element of namedBindings.elements) {\n const localName = element.name.text;\n const importedName = element.propertyName?.text ?? localName;\n result[localName] = importedName;\n found = true;\n }\n }\n\n return found ? result : null;\n};\n\n/**\n * Returns the local alias used in this file for `originalName` when imported\n * from `packageName`, or `null` if the component is not imported.\n */\nexport const getLocalAliasFor = (\n sourceFile: ts.SourceFile,\n packageName: string,\n originalName: string\n): string | null => {\n const aliases = getImportedAliases(sourceFile, packageName);\n if (aliases === null) return null;\n for (const [local, original] of Object.entries(aliases)) {\n if (original === originalName) return local;\n }\n return null;\n};\n\nexport type JsxElementMatch = {\n /** The opening JSX element (the one carrying the attributes). */\n openingElement: ts.JsxOpeningElement | ts.JsxSelfClosingElement;\n /** The full element including children, or the self-closing element. */\n element: ts.JsxElement | ts.JsxSelfClosingElement;\n};\n\n/**\n * Finds every JSX element whose tag name resolves to one of `tagNames`\n * (typically the local aliases returned by {@link getImportedAliases}).\n *\n * The result intentionally includes both opening and full elements so callers\n * can manipulate attributes (via `openingElement`) or wrap/replace the whole\n * element (via `element`).\n */\nexport const findJsxElements = (\n sourceFile: ts.SourceFile,\n tagNames: ReadonlyArray<string>\n): ReadonlyArray<JsxElementMatch> => {\n const matches: Array<JsxElementMatch> = [];\n const tagSet = new Set(tagNames);\n\n const visit = (node: ts.Node): void => {\n if (ts.isJsxSelfClosingElement(node)) {\n if (ts.isIdentifier(node.tagName) && tagSet.has(node.tagName.text)) {\n matches.push({ openingElement: node, element: node });\n }\n } else if (ts.isJsxElement(node)) {\n const opening = node.openingElement;\n if (ts.isIdentifier(opening.tagName) && tagSet.has(opening.tagName.text)) {\n matches.push({ openingElement: opening, element: node });\n }\n }\n ts.forEachChild(node, visit);\n };\n\n visit(sourceFile);\n return matches;\n};\n\n/**\n * Looks up a named JSX attribute on the opening element. Returns `null` when\n * the attribute is missing or part of a spread attribute (which we cannot\n * statically inspect).\n */\nexport const findJsxAttribute = (\n openingElement: ts.JsxOpeningElement | ts.JsxSelfClosingElement,\n attributeName: string\n): ts.JsxAttribute | null => {\n for (const attr of openingElement.attributes.properties) {\n if (!ts.isJsxAttribute(attr)) continue;\n if (!ts.isIdentifier(attr.name)) continue;\n if (attr.name.text === attributeName) return attr;\n }\n return null;\n};\n\n/**\n * `true` when the element forwards a spread expression such as `{...rest}`,\n * which means we can't be sure which attributes are actually set.\n */\nexport const hasSpreadAttribute = (openingElement: ts.JsxOpeningElement | ts.JsxSelfClosingElement): boolean => {\n return openingElement.attributes.properties.some(prop => ts.isJsxSpreadAttribute(prop));\n};\n\n/**\n * Parses the file as TSX so JSX is recognised. We do not need a full program\n * here; tsquery's `ast` helper produces a script-kind source file which loses\n * JSX context, so we go through `createSourceFile` directly.\n */\nexport const parseTsx = (content: string, fileName = \"file.tsx\"): ts.SourceFile =>\n ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);\n\n/** Re-exported for codemods that need direct AST queries. */\nexport { tsquery };\n\n/**\n * Helper for codemods to log a one-line summary at the end of a run. Migration\n * runners aggregate logs per migration, so this keeps output focused.\n */\nexport const logSummary = (migrationName: string, touched: number, manualReviewNeeded?: number): void => {\n if (touched === 0) {\n logger.info(` ${migrationName}: no files changed`);\n return;\n }\n const reviewSuffix =\n manualReviewNeeded !== undefined && manualReviewNeeded > 0\n ? ` (${manualReviewNeeded} location(s) need manual review)`\n : \"\";\n logger.info(` ${migrationName}: updated ${touched} file(s)${reviewSuffix}`);\n};\n\n/**\n * Inserts an attribute string immediately after the tag name (so the new\n * attribute appears first). Returns the resulting source content.\n *\n * The added attribute is rendered verbatim, so callers are responsible for\n * including the leading space (e.g. ` title=\"Close\"`).\n */\nexport const insertAttributeIntoOpeningTag = (\n content: string,\n openingElement: ts.JsxOpeningElement | ts.JsxSelfClosingElement,\n attributeText: string\n): string => {\n const tagName = openingElement.tagName;\n const insertPos = tagName.getEnd();\n return content.slice(0, insertPos) + ` ${attributeText}` + content.slice(insertPos);\n};\n"]}
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.breadcrumbRemoveDeprecatedProps = void 0;
4
+ const devkit_1 = require("@nx/devkit");
5
+ const jsx_utils_1 = require("../utils/jsx-utils");
6
+ const PACKAGE_NAME = "@trackunit/react-components";
7
+ const COMPONENT_NAMES = ["Breadcrumb", "BreadcrumbContainer", "BreadcrumbForMediumScreen"];
8
+ const PROPS_TO_REMOVE = ["backButtonTitle", "expandCollapsedItemsTitle"];
9
+ const stripDeprecatedProps = (filePath, content) => {
10
+ const sourceFile = (0, jsx_utils_1.parseTsx)(content, filePath);
11
+ const aliases = (0, jsx_utils_1.getImportedAliases)(sourceFile, PACKAGE_NAME);
12
+ if (aliases === null)
13
+ return null;
14
+ const localTagNames = [];
15
+ for (const [local, original] of Object.entries(aliases)) {
16
+ if (COMPONENT_NAMES.some(name => name === original)) {
17
+ localTagNames.push(local);
18
+ }
19
+ }
20
+ if (localTagNames.length === 0)
21
+ return null;
22
+ const matches = (0, jsx_utils_1.findJsxElements)(sourceFile, localTagNames);
23
+ if (matches.length === 0)
24
+ return null;
25
+ // Collect every (start, end) range we want to delete. We process them in
26
+ // reverse so earlier offsets stay valid while we splice.
27
+ const ranges = [];
28
+ for (const { openingElement } of matches) {
29
+ for (const propName of PROPS_TO_REMOVE) {
30
+ const attr = (0, jsx_utils_1.findJsxAttribute)(openingElement, propName);
31
+ if (attr === null)
32
+ continue;
33
+ // Drop trailing whitespace + the attribute itself so we don't leave a
34
+ // dangling space inside the opening tag. e.g. `<X foo="a" bar="b" />`
35
+ // becomes `<X foo="a" />` and `<X bar="b" />` becomes `<X />`.
36
+ let start = attr.getFullStart();
37
+ const end = attr.getEnd();
38
+ // getFullStart includes leading whitespace which is what we want.
39
+ // Guard against negative ranges if multiple props collapse later.
40
+ if (start < 0)
41
+ start = attr.getStart();
42
+ ranges.push({ start, end });
43
+ }
44
+ }
45
+ if (ranges.length === 0)
46
+ return null;
47
+ ranges.sort((a, b) => b.start - a.start);
48
+ let updated = content;
49
+ for (const { start, end } of ranges) {
50
+ updated = updated.slice(0, start) + updated.slice(end);
51
+ }
52
+ return updated;
53
+ };
54
+ /**
55
+ * Removes the no-longer-supported `backButtonTitle` and
56
+ * `expandCollapsedItemsTitle` props from `<Breadcrumb>`,
57
+ * `<BreadcrumbContainer>`, and `<BreadcrumbForMediumScreen>`. Their accessible
58
+ * names are now sourced from the `react-components` library translations.
59
+ */
60
+ const breadcrumbRemoveDeprecatedProps = (tree) => {
61
+ const touched = (0, jsx_utils_1.visitTsxFiles)(tree, "Breadcrumb", stripDeprecatedProps);
62
+ (0, jsx_utils_1.logSummary)("breadcrumb-remove-deprecated-props", touched);
63
+ devkit_1.logger.info(" Ensure the host application registers the @trackunit/react-components library translations so the back/expand buttons render localized labels.");
64
+ };
65
+ exports.breadcrumbRemoveDeprecatedProps = breadcrumbRemoveDeprecatedProps;
66
+ exports.default = exports.breadcrumbRemoveDeprecatedProps;
67
+ //# sourceMappingURL=breadcrumb-remove-deprecated-props.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"breadcrumb-remove-deprecated-props.js","sourceRoot":"","sources":["../../../../../../libs/react/components/migrations/v2-0-0/breadcrumb-remove-deprecated-props.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Tree, logger } from \"@nx/devkit\";\nimport {\n findJsxAttribute,\n findJsxElements,\n getImportedAliases,\n logSummary,\n parseTsx,\n visitTsxFiles,\n} from \"../utils/jsx-utils\";\n\nconst PACKAGE_NAME = \"@trackunit/react-components\";\nconst COMPONENT_NAMES = [\"Breadcrumb\", \"BreadcrumbContainer\", \"BreadcrumbForMediumScreen\"] as const;\nconst PROPS_TO_REMOVE = [\"backButtonTitle\", \"expandCollapsedItemsTitle\"] as const;\n\nconst stripDeprecatedProps = (filePath: string, content: string): string | null => {\n const sourceFile = parseTsx(content, filePath);\n\n const aliases = getImportedAliases(sourceFile, PACKAGE_NAME);\n if (aliases === null) return null;\n\n const localTagNames: Array<string> = [];\n for (const [local, original] of Object.entries(aliases)) {\n if (COMPONENT_NAMES.some(name => name === original)) {\n localTagNames.push(local);\n }\n }\n if (localTagNames.length === 0) return null;\n\n const matches = findJsxElements(sourceFile, localTagNames);\n if (matches.length === 0) return null;\n\n // Collect every (start, end) range we want to delete. We process them in\n // reverse so earlier offsets stay valid while we splice.\n const ranges: Array<{ start: number; end: number }> = [];\n for (const { openingElement } of matches) {\n for (const propName of PROPS_TO_REMOVE) {\n const attr = findJsxAttribute(openingElement, propName);\n if (attr === null) continue;\n\n // Drop trailing whitespace + the attribute itself so we don't leave a\n // dangling space inside the opening tag. e.g. `<X foo=\"a\" bar=\"b\" />`\n // becomes `<X foo=\"a\" />` and `<X bar=\"b\" />` becomes `<X />`.\n let start = attr.getFullStart();\n const end = attr.getEnd();\n // getFullStart includes leading whitespace which is what we want.\n // Guard against negative ranges if multiple props collapse later.\n if (start < 0) start = attr.getStart();\n ranges.push({ start, end });\n }\n }\n\n if (ranges.length === 0) return null;\n\n ranges.sort((a, b) => b.start - a.start);\n\n let updated = content;\n for (const { start, end } of ranges) {\n updated = updated.slice(0, start) + updated.slice(end);\n }\n\n return updated;\n};\n\n/**\n * Removes the no-longer-supported `backButtonTitle` and\n * `expandCollapsedItemsTitle` props from `<Breadcrumb>`,\n * `<BreadcrumbContainer>`, and `<BreadcrumbForMediumScreen>`. Their accessible\n * names are now sourced from the `react-components` library translations.\n */\nexport const breadcrumbRemoveDeprecatedProps = (tree: Tree): void => {\n const touched = visitTsxFiles(tree, \"Breadcrumb\", stripDeprecatedProps);\n logSummary(\"breadcrumb-remove-deprecated-props\", touched);\n logger.info(\n \" Ensure the host application registers the @trackunit/react-components library translations so the back/expand buttons render localized labels.\"\n );\n};\n\nexport default breadcrumbRemoveDeprecatedProps;\n"]}
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cardHeaderOnClickCloseToActions = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const ts = tslib_1.__importStar(require("typescript"));
6
+ const jsx_utils_1 = require("../utils/jsx-utils");
7
+ const PACKAGE_NAME = "@trackunit/react-components";
8
+ const COMPONENT_NAME = "CardHeader";
9
+ const CLOSE_BUTTON_NAMES = ["IconButton", "Icon"];
10
+ const renderCloseButton = (handlerExpr, iconAlias, iconButtonAlias) => `<${iconButtonAlias} icon={<${iconAlias} name="XMark" size="small" />} onClick={${handlerExpr}} title="Close" variant="ghost-neutral" />`;
11
+ const ensureNamedImport = (content, packageName, namesToAdd) => {
12
+ const sourceFile = (0, jsx_utils_1.parseTsx)(content, "source.tsx");
13
+ const aliases = (0, jsx_utils_1.getImportedAliases)(sourceFile, packageName) ?? {};
14
+ const missing = namesToAdd.filter(name => !Object.values(aliases).includes(name));
15
+ if (missing.length === 0)
16
+ return content;
17
+ for (const stmt of sourceFile.statements) {
18
+ if (!ts.isImportDeclaration(stmt))
19
+ continue;
20
+ const moduleSpecifier = stmt.moduleSpecifier;
21
+ if (!ts.isStringLiteral(moduleSpecifier) || moduleSpecifier.text !== packageName)
22
+ continue;
23
+ const namedBindings = stmt.importClause?.namedBindings;
24
+ if (namedBindings === undefined || !ts.isNamedImports(namedBindings))
25
+ continue;
26
+ const elementsText = namedBindings.elements.map(el => el.getText()).join(", ");
27
+ const merged = [elementsText, ...missing].join(", ");
28
+ const before = content.slice(0, namedBindings.getStart());
29
+ const after = content.slice(namedBindings.getEnd());
30
+ return `${before}{ ${merged} }${after}`;
31
+ }
32
+ // No existing import from the package; add a fresh line near the top.
33
+ const importLine = `import { ${missing.join(", ")} } from "${packageName}";\n`;
34
+ return importLine + content;
35
+ };
36
+ const transformCardHeaderUsage = (filePath, content) => {
37
+ const sourceFile = (0, jsx_utils_1.parseTsx)(content, filePath);
38
+ const aliases = (0, jsx_utils_1.getImportedAliases)(sourceFile, PACKAGE_NAME);
39
+ if (aliases === null)
40
+ return null;
41
+ const cardHeaderAlias = Object.entries(aliases).find(([, name]) => name === COMPONENT_NAME)?.[0];
42
+ if (cardHeaderAlias === undefined)
43
+ return null;
44
+ const matches = (0, jsx_utils_1.findJsxElements)(sourceFile, [cardHeaderAlias]);
45
+ if (matches.length === 0)
46
+ return null;
47
+ const edits = [];
48
+ for (const { openingElement } of matches) {
49
+ const onClickCloseAttr = (0, jsx_utils_1.findJsxAttribute)(openingElement, "onClickClose");
50
+ if (onClickCloseAttr === null)
51
+ continue;
52
+ const initializer = onClickCloseAttr.initializer;
53
+ if (initializer === undefined)
54
+ continue;
55
+ let handlerExpr;
56
+ if (ts.isJsxExpression(initializer) && initializer.expression !== undefined) {
57
+ handlerExpr = initializer.expression.getText();
58
+ }
59
+ else if (ts.isStringLiteral(initializer)) {
60
+ handlerExpr = `"${initializer.text}"`;
61
+ }
62
+ else {
63
+ continue;
64
+ }
65
+ const actionsAttr = (0, jsx_utils_1.findJsxAttribute)(openingElement, "actions");
66
+ if (actionsAttr !== null) {
67
+ // Don't try to merge with an existing actions prop; flag and skip.
68
+ continue;
69
+ }
70
+ const replacement = `actions={${renderCloseButton(handlerExpr, "Icon", "IconButton")}}`;
71
+ edits.push({
72
+ start: onClickCloseAttr.getStart(),
73
+ end: onClickCloseAttr.getEnd(),
74
+ replacement,
75
+ });
76
+ }
77
+ if (edits.length === 0)
78
+ return null;
79
+ edits.sort((a, b) => b.start - a.start);
80
+ let updated = content;
81
+ for (const { start, end, replacement } of edits) {
82
+ updated = updated.slice(0, start) + replacement + updated.slice(end);
83
+ }
84
+ updated = ensureNamedImport(updated, PACKAGE_NAME, [...CLOSE_BUTTON_NAMES]);
85
+ return updated;
86
+ };
87
+ /**
88
+ * Replaces the removed `onClickClose` prop on `<CardHeader>` with the
89
+ * equivalent action rendered through the `actions` slot using `IconButton` +
90
+ * the `XMark` icon.
91
+ *
92
+ * Cards that already declared `actions` are skipped to avoid clobbering
93
+ * existing markup.
94
+ */
95
+ const cardHeaderOnClickCloseToActions = (tree) => {
96
+ const touched = (0, jsx_utils_1.visitTsxFiles)(tree, "CardHeader", transformCardHeaderUsage);
97
+ (0, jsx_utils_1.logSummary)("cardheader-onclickclose-to-actions", touched);
98
+ };
99
+ exports.cardHeaderOnClickCloseToActions = cardHeaderOnClickCloseToActions;
100
+ exports.default = exports.cardHeaderOnClickCloseToActions;
101
+ //# sourceMappingURL=cardheader-onclickclose-to-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cardheader-onclickclose-to-actions.js","sourceRoot":"","sources":["../../../../../../libs/react/components/migrations/v2-0-0/cardheader-onclickclose-to-actions.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Tree } from \"@nx/devkit\";\nimport * as ts from \"typescript\";\nimport {\n findJsxAttribute,\n findJsxElements,\n getImportedAliases,\n logSummary,\n parseTsx,\n visitTsxFiles,\n} from \"../utils/jsx-utils\";\n\nconst PACKAGE_NAME = \"@trackunit/react-components\";\nconst COMPONENT_NAME = \"CardHeader\";\n\nconst CLOSE_BUTTON_NAMES = [\"IconButton\", \"Icon\"] as const;\n\nconst renderCloseButton = (handlerExpr: string, iconAlias: string, iconButtonAlias: string): string =>\n `<${iconButtonAlias} icon={<${iconAlias} name=\"XMark\" size=\"small\" />} onClick={${handlerExpr}} title=\"Close\" variant=\"ghost-neutral\" />`;\n\nconst ensureNamedImport = (content: string, packageName: string, namesToAdd: ReadonlyArray<string>): string => {\n const sourceFile = parseTsx(content, \"source.tsx\");\n const aliases = getImportedAliases(sourceFile, packageName) ?? {};\n\n const missing = namesToAdd.filter(name => !Object.values(aliases).includes(name));\n if (missing.length === 0) return content;\n\n for (const stmt of sourceFile.statements) {\n if (!ts.isImportDeclaration(stmt)) continue;\n const moduleSpecifier = stmt.moduleSpecifier;\n if (!ts.isStringLiteral(moduleSpecifier) || moduleSpecifier.text !== packageName) continue;\n\n const namedBindings = stmt.importClause?.namedBindings;\n if (namedBindings === undefined || !ts.isNamedImports(namedBindings)) continue;\n\n const elementsText = namedBindings.elements.map(el => el.getText()).join(\", \");\n const merged = [elementsText, ...missing].join(\", \");\n const before = content.slice(0, namedBindings.getStart());\n const after = content.slice(namedBindings.getEnd());\n return `${before}{ ${merged} }${after}`;\n }\n\n // No existing import from the package; add a fresh line near the top.\n const importLine = `import { ${missing.join(\", \")} } from \"${packageName}\";\\n`;\n return importLine + content;\n};\n\nconst transformCardHeaderUsage = (filePath: string, content: string): string | null => {\n const sourceFile = parseTsx(content, filePath);\n\n const aliases = getImportedAliases(sourceFile, PACKAGE_NAME);\n if (aliases === null) return null;\n\n const cardHeaderAlias = Object.entries(aliases).find(([, name]) => name === COMPONENT_NAME)?.[0];\n if (cardHeaderAlias === undefined) return null;\n\n const matches = findJsxElements(sourceFile, [cardHeaderAlias]);\n if (matches.length === 0) return null;\n\n type Edit = { start: number; end: number; replacement: string };\n const edits: Array<Edit> = [];\n\n for (const { openingElement } of matches) {\n const onClickCloseAttr = findJsxAttribute(openingElement, \"onClickClose\");\n if (onClickCloseAttr === null) continue;\n\n const initializer = onClickCloseAttr.initializer;\n if (initializer === undefined) continue;\n\n let handlerExpr: string;\n if (ts.isJsxExpression(initializer) && initializer.expression !== undefined) {\n handlerExpr = initializer.expression.getText();\n } else if (ts.isStringLiteral(initializer)) {\n handlerExpr = `\"${initializer.text}\"`;\n } else {\n continue;\n }\n\n const actionsAttr = findJsxAttribute(openingElement, \"actions\");\n if (actionsAttr !== null) {\n // Don't try to merge with an existing actions prop; flag and skip.\n continue;\n }\n\n const replacement = `actions={${renderCloseButton(handlerExpr, \"Icon\", \"IconButton\")}}`;\n edits.push({\n start: onClickCloseAttr.getStart(),\n end: onClickCloseAttr.getEnd(),\n replacement,\n });\n }\n\n if (edits.length === 0) return null;\n\n edits.sort((a, b) => b.start - a.start);\n\n let updated = content;\n for (const { start, end, replacement } of edits) {\n updated = updated.slice(0, start) + replacement + updated.slice(end);\n }\n\n updated = ensureNamedImport(updated, PACKAGE_NAME, [...CLOSE_BUTTON_NAMES]);\n\n return updated;\n};\n\n/**\n * Replaces the removed `onClickClose` prop on `<CardHeader>` with the\n * equivalent action rendered through the `actions` slot using `IconButton` +\n * the `XMark` icon.\n *\n * Cards that already declared `actions` are skipped to avoid clobbering\n * existing markup.\n */\nexport const cardHeaderOnClickCloseToActions = (tree: Tree): void => {\n const touched = visitTsxFiles(tree, \"CardHeader\", transformCardHeaderUsage);\n logSummary(\"cardheader-onclickclose-to-actions\", touched);\n};\n\nexport default cardHeaderOnClickCloseToActions;\n"]}
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.iconButtonAddRequiredName = void 0;
4
+ const devkit_1 = require("@nx/devkit");
5
+ const jsx_utils_1 = require("../utils/jsx-utils");
6
+ const PACKAGE_NAME = "@trackunit/react-components";
7
+ const COMPONENT_NAME = "IconButton";
8
+ const PLACEHOLDER_TITLE = "TODO Replace with localized button title";
9
+ const transformIconButtonUsage = (filePath, content) => {
10
+ const sourceFile = (0, jsx_utils_1.parseTsx)(content, filePath);
11
+ const aliases = (0, jsx_utils_1.getImportedAliases)(sourceFile, PACKAGE_NAME);
12
+ if (aliases === null)
13
+ return null;
14
+ const localAlias = Object.entries(aliases).find(([, name]) => name === COMPONENT_NAME)?.[0];
15
+ if (localAlias === undefined)
16
+ return null;
17
+ const matches = (0, jsx_utils_1.findJsxElements)(sourceFile, [localAlias]);
18
+ if (matches.length === 0)
19
+ return null;
20
+ const edits = [];
21
+ for (const { openingElement } of matches) {
22
+ if ((0, jsx_utils_1.hasSpreadAttribute)(openingElement))
23
+ continue;
24
+ const hasTitle = (0, jsx_utils_1.findJsxAttribute)(openingElement, "title") !== null;
25
+ const hasAriaLabel = (0, jsx_utils_1.findJsxAttribute)(openingElement, "ariaLabel") !== null;
26
+ if (hasTitle || hasAriaLabel)
27
+ continue;
28
+ edits.push({
29
+ offset: openingElement.tagName.getEnd(),
30
+ text: ` title="${PLACEHOLDER_TITLE}"`,
31
+ });
32
+ }
33
+ if (edits.length === 0)
34
+ return null;
35
+ edits.sort((a, b) => (a.offset === b.offset ? 0 : b.offset - a.offset));
36
+ let updated = content;
37
+ for (const { offset, text } of edits) {
38
+ updated = updated.slice(0, offset) + text + updated.slice(offset);
39
+ }
40
+ return updated;
41
+ };
42
+ /**
43
+ * Adds a placeholder `title` to `<IconButton>` usages that don't already
44
+ * provide either `title` or `ariaLabel`. The new accessible name requirement
45
+ * means every icon button must carry one of these two props.
46
+ *
47
+ * Usages with a spread attribute (e.g. `<IconButton {...props} />`) are
48
+ * skipped because we cannot tell whether `title`/`ariaLabel` is forwarded.
49
+ */
50
+ const iconButtonAddRequiredName = (tree) => {
51
+ const touched = (0, jsx_utils_1.visitTsxFiles)(tree, "IconButton", transformIconButtonUsage);
52
+ (0, jsx_utils_1.logSummary)("iconbutton-add-required-name", touched);
53
+ if (touched > 0) {
54
+ devkit_1.logger.info(` Replace the placeholder "${PLACEHOLDER_TITLE}" titles with localized strings (or switch to ariaLabel where appropriate).`);
55
+ }
56
+ };
57
+ exports.iconButtonAddRequiredName = iconButtonAddRequiredName;
58
+ exports.default = exports.iconButtonAddRequiredName;
59
+ //# sourceMappingURL=iconbutton-add-required-name.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iconbutton-add-required-name.js","sourceRoot":"","sources":["../../../../../../libs/react/components/migrations/v2-0-0/iconbutton-add-required-name.ts"],"names":[],"mappings":";;;AAAA,uCAA+C;AAC/C,kDAQ4B;AAE5B,MAAM,YAAY,GAAG,6BAA6B,CAAC;AACnD,MAAM,cAAc,GAAG,YAAY,CAAC;AAEpC,MAAM,iBAAiB,GAAG,0CAA0C,CAAC;AAErE,MAAM,wBAAwB,GAAG,CAAC,QAAgB,EAAE,OAAe,EAAiB,EAAE;IACpF,MAAM,UAAU,GAAG,IAAA,oBAAQ,EAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAE/C,MAAM,OAAO,GAAG,IAAA,8BAAkB,EAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAC7D,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAElC,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,KAAK,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5F,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAE1C,MAAM,OAAO,GAAG,IAAA,2BAAe,EAAC,UAAU,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IAC1D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAGtC,MAAM,KAAK,GAAgB,EAAE,CAAC;IAE9B,KAAK,MAAM,EAAE,cAAc,EAAE,IAAI,OAAO,EAAE,CAAC;QACzC,IAAI,IAAA,8BAAkB,EAAC,cAAc,CAAC;YAAE,SAAS;QAEjD,MAAM,QAAQ,GAAG,IAAA,4BAAgB,EAAC,cAAc,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QACpE,MAAM,YAAY,GAAG,IAAA,4BAAgB,EAAC,cAAc,EAAE,WAAW,CAAC,KAAK,IAAI,CAAC;QAC5E,IAAI,QAAQ,IAAI,YAAY;YAAE,SAAS;QAEvC,KAAK,CAAC,IAAI,CAAC;YACT,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC,MAAM,EAAE;YACvC,IAAI,EAAE,WAAW,iBAAiB,GAAG;SACtC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEpC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IAExE,IAAI,OAAO,GAAG,OAAO,CAAC;IACtB,KAAK,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,KAAK,EAAE,CAAC;QACrC,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF;;;;;;;GAOG;AACI,MAAM,yBAAyB,GAAG,CAAC,IAAU,EAAQ,EAAE;IAC5D,MAAM,OAAO,GAAG,IAAA,yBAAa,EAAC,IAAI,EAAE,YAAY,EAAE,wBAAwB,CAAC,CAAC;IAC5E,IAAA,sBAAU,EAAC,8BAA8B,EAAE,OAAO,CAAC,CAAC;IACpD,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAChB,eAAM,CAAC,IAAI,CACT,8BAA8B,iBAAiB,6EAA6E,CAC7H,CAAC;IACJ,CAAC;AACH,CAAC,CAAC;AARW,QAAA,yBAAyB,6BAQpC;AAEF,kBAAe,iCAAyB,CAAC","sourcesContent":["import { type Tree, logger } from \"@nx/devkit\";\nimport {\n findJsxAttribute,\n findJsxElements,\n getImportedAliases,\n hasSpreadAttribute,\n logSummary,\n parseTsx,\n visitTsxFiles,\n} from \"../utils/jsx-utils\";\n\nconst PACKAGE_NAME = \"@trackunit/react-components\";\nconst COMPONENT_NAME = \"IconButton\";\n\nconst PLACEHOLDER_TITLE = \"TODO Replace with localized button title\";\n\nconst transformIconButtonUsage = (filePath: string, content: string): string | null => {\n const sourceFile = parseTsx(content, filePath);\n\n const aliases = getImportedAliases(sourceFile, PACKAGE_NAME);\n if (aliases === null) return null;\n\n const localAlias = Object.entries(aliases).find(([, name]) => name === COMPONENT_NAME)?.[0];\n if (localAlias === undefined) return null;\n\n const matches = findJsxElements(sourceFile, [localAlias]);\n if (matches.length === 0) return null;\n\n type Edit = { offset: number; text: string };\n const edits: Array<Edit> = [];\n\n for (const { openingElement } of matches) {\n if (hasSpreadAttribute(openingElement)) continue;\n\n const hasTitle = findJsxAttribute(openingElement, \"title\") !== null;\n const hasAriaLabel = findJsxAttribute(openingElement, \"ariaLabel\") !== null;\n if (hasTitle || hasAriaLabel) continue;\n\n edits.push({\n offset: openingElement.tagName.getEnd(),\n text: ` title=\"${PLACEHOLDER_TITLE}\"`,\n });\n }\n\n if (edits.length === 0) return null;\n\n edits.sort((a, b) => (a.offset === b.offset ? 0 : b.offset - a.offset));\n\n let updated = content;\n for (const { offset, text } of edits) {\n updated = updated.slice(0, offset) + text + updated.slice(offset);\n }\n return updated;\n};\n\n/**\n * Adds a placeholder `title` to `<IconButton>` usages that don't already\n * provide either `title` or `ariaLabel`. The new accessible name requirement\n * means every icon button must carry one of these two props.\n *\n * Usages with a spread attribute (e.g. `<IconButton {...props} />`) are\n * skipped because we cannot tell whether `title`/`ariaLabel` is forwarded.\n */\nexport const iconButtonAddRequiredName = (tree: Tree): void => {\n const touched = visitTsxFiles(tree, \"IconButton\", transformIconButtonUsage);\n logSummary(\"iconbutton-add-required-name\", touched);\n if (touched > 0) {\n logger.info(\n ` Replace the placeholder \"${PLACEHOLDER_TITLE}\" titles with localized strings (or switch to ariaLabel where appropriate).`\n );\n }\n};\n\nexport default iconButtonAddRequiredName;\n"]}
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.kpiTooltipLabelToWrapper = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const ts = tslib_1.__importStar(require("typescript"));
6
+ const jsx_utils_1 = require("../utils/jsx-utils");
7
+ const PACKAGE_NAME = "@trackunit/react-components";
8
+ const COMPONENT_NAME = "KPI";
9
+ const ensureTooltipImport = (content) => {
10
+ const sourceFile = (0, jsx_utils_1.parseTsx)(content, "source.tsx");
11
+ const aliases = (0, jsx_utils_1.getImportedAliases)(sourceFile, PACKAGE_NAME);
12
+ if (aliases !== null && Object.values(aliases).includes("Tooltip"))
13
+ return content;
14
+ for (const stmt of sourceFile.statements) {
15
+ if (!ts.isImportDeclaration(stmt))
16
+ continue;
17
+ const moduleSpecifier = stmt.moduleSpecifier;
18
+ if (!ts.isStringLiteral(moduleSpecifier) || moduleSpecifier.text !== PACKAGE_NAME)
19
+ continue;
20
+ const namedBindings = stmt.importClause?.namedBindings;
21
+ if (namedBindings === undefined || !ts.isNamedImports(namedBindings))
22
+ continue;
23
+ const elementsText = namedBindings.elements.map(el => el.getText()).join(", ");
24
+ const merged = `${elementsText}, Tooltip`;
25
+ const before = content.slice(0, namedBindings.getStart());
26
+ const after = content.slice(namedBindings.getEnd());
27
+ return `${before}{ ${merged} }${after}`;
28
+ }
29
+ return `import { Tooltip } from "${PACKAGE_NAME}";\n${content}`;
30
+ };
31
+ const renderTooltipExpression = (label) => label;
32
+ const transformKpiUsage = (filePath, content) => {
33
+ const sourceFile = (0, jsx_utils_1.parseTsx)(content, filePath);
34
+ const aliases = (0, jsx_utils_1.getImportedAliases)(sourceFile, PACKAGE_NAME);
35
+ if (aliases === null)
36
+ return null;
37
+ const localAlias = Object.entries(aliases).find(([, name]) => name === COMPONENT_NAME)?.[0];
38
+ if (localAlias === undefined)
39
+ return null;
40
+ const matches = (0, jsx_utils_1.findJsxElements)(sourceFile, [localAlias]);
41
+ if (matches.length === 0)
42
+ return null;
43
+ const edits = [];
44
+ for (const { openingElement, element } of matches) {
45
+ const tooltipLabelAttr = (0, jsx_utils_1.findJsxAttribute)(openingElement, "tooltipLabel");
46
+ if (tooltipLabelAttr === null)
47
+ continue;
48
+ const initializer = tooltipLabelAttr.initializer;
49
+ if (initializer === undefined)
50
+ continue;
51
+ let label;
52
+ if (ts.isStringLiteral(initializer)) {
53
+ label = `"${initializer.text}"`;
54
+ }
55
+ else if (ts.isJsxExpression(initializer) && initializer.expression !== undefined) {
56
+ label = `{${initializer.expression.getText()}}`;
57
+ }
58
+ else {
59
+ continue;
60
+ }
61
+ edits.push({
62
+ elementStart: element.getStart(),
63
+ elementEnd: element.getEnd(),
64
+ attrStart: tooltipLabelAttr.getFullStart(),
65
+ attrEnd: tooltipLabelAttr.getEnd(),
66
+ label,
67
+ elementText: content.slice(element.getStart(), element.getEnd()),
68
+ });
69
+ }
70
+ if (edits.length === 0)
71
+ return null;
72
+ edits.sort((a, b) => b.elementStart - a.elementStart);
73
+ let updated = content;
74
+ for (const edit of edits) {
75
+ // Remove the tooltipLabel attribute first (it's inside the element).
76
+ const elementWithoutAttr = edit.elementText.slice(0, edit.attrStart - edit.elementStart) +
77
+ edit.elementText.slice(edit.attrEnd - edit.elementStart);
78
+ const wrapped = `<Tooltip label=${renderTooltipExpression(edit.label)} placement="bottom">${elementWithoutAttr}</Tooltip>`;
79
+ updated = updated.slice(0, edit.elementStart) + wrapped + updated.slice(edit.elementEnd);
80
+ }
81
+ updated = ensureTooltipImport(updated);
82
+ return updated;
83
+ };
84
+ /**
85
+ * Replaces the removed `tooltipLabel` prop on `<KPI>` with an explicit
86
+ * `<Tooltip>` wrapper. The `KPICard` variant is intentionally not touched
87
+ * because it still owns its own `tooltipLabel` prop.
88
+ */
89
+ const kpiTooltipLabelToWrapper = (tree) => {
90
+ const touched = (0, jsx_utils_1.visitTsxFiles)(tree, "tooltipLabel", transformKpiUsage);
91
+ (0, jsx_utils_1.logSummary)("kpi-tooltiplabel-to-wrapper", touched);
92
+ };
93
+ exports.kpiTooltipLabelToWrapper = kpiTooltipLabelToWrapper;
94
+ exports.default = exports.kpiTooltipLabelToWrapper;
95
+ //# sourceMappingURL=kpi-tooltiplabel-to-wrapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kpi-tooltiplabel-to-wrapper.js","sourceRoot":"","sources":["../../../../../../libs/react/components/migrations/v2-0-0/kpi-tooltiplabel-to-wrapper.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Tree } from \"@nx/devkit\";\nimport * as ts from \"typescript\";\nimport {\n findJsxAttribute,\n findJsxElements,\n getImportedAliases,\n logSummary,\n parseTsx,\n visitTsxFiles,\n} from \"../utils/jsx-utils\";\n\nconst PACKAGE_NAME = \"@trackunit/react-components\";\nconst COMPONENT_NAME = \"KPI\";\n\nconst ensureTooltipImport = (content: string): string => {\n const sourceFile = parseTsx(content, \"source.tsx\");\n const aliases = getImportedAliases(sourceFile, PACKAGE_NAME);\n if (aliases !== null && Object.values(aliases).includes(\"Tooltip\")) return content;\n\n for (const stmt of sourceFile.statements) {\n if (!ts.isImportDeclaration(stmt)) continue;\n const moduleSpecifier = stmt.moduleSpecifier;\n if (!ts.isStringLiteral(moduleSpecifier) || moduleSpecifier.text !== PACKAGE_NAME) continue;\n\n const namedBindings = stmt.importClause?.namedBindings;\n if (namedBindings === undefined || !ts.isNamedImports(namedBindings)) continue;\n\n const elementsText = namedBindings.elements.map(el => el.getText()).join(\", \");\n const merged = `${elementsText}, Tooltip`;\n const before = content.slice(0, namedBindings.getStart());\n const after = content.slice(namedBindings.getEnd());\n return `${before}{ ${merged} }${after}`;\n }\n\n return `import { Tooltip } from \"${PACKAGE_NAME}\";\\n${content}`;\n};\n\nconst renderTooltipExpression = (label: string): string => label;\n\nconst transformKpiUsage = (filePath: string, content: string): string | null => {\n const sourceFile = parseTsx(content, filePath);\n\n const aliases = getImportedAliases(sourceFile, PACKAGE_NAME);\n if (aliases === null) return null;\n\n const localAlias = Object.entries(aliases).find(([, name]) => name === COMPONENT_NAME)?.[0];\n if (localAlias === undefined) return null;\n\n const matches = findJsxElements(sourceFile, [localAlias]);\n if (matches.length === 0) return null;\n\n type Edit = {\n elementStart: number;\n elementEnd: number;\n attrStart: number;\n attrEnd: number;\n label: string;\n elementText: string;\n };\n const edits: Array<Edit> = [];\n\n for (const { openingElement, element } of matches) {\n const tooltipLabelAttr = findJsxAttribute(openingElement, \"tooltipLabel\");\n if (tooltipLabelAttr === null) continue;\n\n const initializer = tooltipLabelAttr.initializer;\n if (initializer === undefined) continue;\n\n let label: string;\n if (ts.isStringLiteral(initializer)) {\n label = `\"${initializer.text}\"`;\n } else if (ts.isJsxExpression(initializer) && initializer.expression !== undefined) {\n label = `{${initializer.expression.getText()}}`;\n } else {\n continue;\n }\n\n edits.push({\n elementStart: element.getStart(),\n elementEnd: element.getEnd(),\n attrStart: tooltipLabelAttr.getFullStart(),\n attrEnd: tooltipLabelAttr.getEnd(),\n label,\n elementText: content.slice(element.getStart(), element.getEnd()),\n });\n }\n\n if (edits.length === 0) return null;\n\n edits.sort((a, b) => b.elementStart - a.elementStart);\n\n let updated = content;\n for (const edit of edits) {\n // Remove the tooltipLabel attribute first (it's inside the element).\n const elementWithoutAttr =\n edit.elementText.slice(0, edit.attrStart - edit.elementStart) +\n edit.elementText.slice(edit.attrEnd - edit.elementStart);\n const wrapped = `<Tooltip label=${renderTooltipExpression(edit.label)} placement=\"bottom\">${elementWithoutAttr}</Tooltip>`;\n updated = updated.slice(0, edit.elementStart) + wrapped + updated.slice(edit.elementEnd);\n }\n\n updated = ensureTooltipImport(updated);\n\n return updated;\n};\n\n/**\n * Replaces the removed `tooltipLabel` prop on `<KPI>` with an explicit\n * `<Tooltip>` wrapper. The `KPICard` variant is intentionally not touched\n * because it still owns its own `tooltipLabel` prop.\n */\nexport const kpiTooltipLabelToWrapper = (tree: Tree): void => {\n const touched = visitTsxFiles(tree, \"tooltipLabel\", transformKpiUsage);\n logSummary(\"kpi-tooltiplabel-to-wrapper\", touched);\n};\n\nexport default kpiTooltipLabelToWrapper;\n"]}