@trackunit/react-components 3.12.4-alpha-10f2d9d2ab3.0 → 4.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.
Files changed (60) hide show
  1. package/index.cjs.js +104 -959
  2. package/index.esm.js +102 -952
  3. package/migrations/assets/react-router-components/package.json +23 -0
  4. package/migrations/utils/move-named-imports.js +409 -0
  5. package/migrations/utils/package-json-utils.js +73 -0
  6. package/migrations/v4-0-0/move-to-router-components.js +71 -0
  7. package/migrations.json +5 -0
  8. package/package.json +6 -7
  9. package/src/components/ExternalLink/ExternalLink.d.ts +2 -2
  10. package/src/index.d.ts +0 -4
  11. package/translation.cjs.js +0 -1
  12. package/translation.esm.js +0 -1
  13. package/translation10.cjs.js +0 -1
  14. package/translation10.esm.js +0 -1
  15. package/translation11.cjs.js +0 -1
  16. package/translation11.esm.js +0 -1
  17. package/translation12.cjs.js +0 -1
  18. package/translation12.esm.js +0 -1
  19. package/translation13.cjs.js +0 -1
  20. package/translation13.esm.js +0 -1
  21. package/translation14.cjs.js +0 -1
  22. package/translation14.esm.js +0 -1
  23. package/translation15.cjs.js +0 -1
  24. package/translation15.esm.js +0 -1
  25. package/translation16.cjs.js +0 -1
  26. package/translation16.esm.js +0 -1
  27. package/translation17.cjs.js +0 -1
  28. package/translation17.esm.js +0 -1
  29. package/translation2.cjs.js +0 -1
  30. package/translation2.esm.js +0 -1
  31. package/translation3.cjs.js +0 -1
  32. package/translation3.esm.js +0 -1
  33. package/translation4.cjs.js +0 -1
  34. package/translation4.esm.js +0 -1
  35. package/translation5.cjs.js +0 -1
  36. package/translation5.esm.js +0 -1
  37. package/translation6.cjs.js +0 -1
  38. package/translation6.esm.js +0 -1
  39. package/translation7.cjs.js +0 -1
  40. package/translation7.esm.js +0 -1
  41. package/translation8.cjs.js +0 -1
  42. package/translation8.esm.js +0 -1
  43. package/translation9.cjs.js +0 -1
  44. package/translation9.esm.js +0 -1
  45. package/src/components/Breadcrumb/Breadcrumb.d.ts +0 -39
  46. package/src/components/Breadcrumb/Breadcrumb.variant.d.ts +0 -45
  47. package/src/components/Breadcrumb/BreadcrumbContainer.d.ts +0 -8
  48. package/src/components/Breadcrumb/BreadcrumbForLargeScreen.d.ts +0 -6
  49. package/src/components/Breadcrumb/BreadcrumbForMediumScreen.d.ts +0 -6
  50. package/src/components/Breadcrumb/BreadcrumbForSmallScreen.d.ts +0 -6
  51. package/src/components/Breadcrumb/BreadcrumbItem.d.ts +0 -6
  52. package/src/components/Breadcrumb/utils/types.d.ts +0 -28
  53. package/src/components/Breadcrumb/utils/useBreadcrumbItemsToRender.d.ts +0 -6
  54. package/src/hooks/persistence/hashFragment.d.ts +0 -70
  55. package/src/hooks/persistence/readPrimitiveSearchParam.d.ts +0 -8
  56. package/src/hooks/persistence/urlIdentity.d.ts +0 -64
  57. package/src/hooks/persistence/usePersistedState.d.ts +0 -117
  58. package/src/hooks/persistence/useSearchParamSync.d.ts +0 -17
  59. package/src/hooks/persistence/useUrlParamSync.d.ts +0 -71
  60. package/src/hooks/useCursorUrlSync.d.ts +0 -23
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@trackunit/react-router-components",
3
+ "version": "1.0.1",
4
+ "repository": "https://github.com/Trackunit/manager",
5
+ "license": "SEE LICENSE IN LICENSE.txt",
6
+ "engines": {
7
+ "node": ">=24.x"
8
+ },
9
+ "dependencies": {
10
+ "@trackunit/css-class-variance-utilities": "2.6.7",
11
+ "@trackunit/i18n-library-translation": "2.12.6",
12
+ "@trackunit/react-components": "4.0.0",
13
+ "@trackunit/react-drawer": "2.14.7",
14
+ "@trackunit/shared-utils": "1.22.7",
15
+ "dequal": "^2.0.3",
16
+ "zod": "^3.25.76"
17
+ },
18
+ "peerDependencies": {
19
+ "@tanstack/react-router": "^1.114.29",
20
+ "react": "^19.0.0"
21
+ },
22
+ "migrations": "./migrations.json"
23
+ }
@@ -0,0 +1,409 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.logManualReview = exports.moveNamedImports = void 0;
37
+ const devkit_1 = require("@nx/devkit");
38
+ const ts = __importStar(require("typescript"));
39
+ const jsx_utils_1 = require("./jsx-utils");
40
+ const MOCK_OBJECTS = new Set(["vi", "jest"]);
41
+ const MOCK_METHODS = new Set([
42
+ "mock",
43
+ "doMock",
44
+ "unmock",
45
+ "doUnmock",
46
+ "importActual",
47
+ "importMock",
48
+ "requireActual",
49
+ "requireMock",
50
+ ]);
51
+ const toRenameMap = (names) => Array.isArray(names) ? new Map(names.map(name => [name, name])) : new Map(Object.entries(names));
52
+ const parseSource = (filePath, content) => ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.TSX);
53
+ const isModule = (node, moduleName) => node !== undefined &&
54
+ (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) &&
55
+ node.text === moduleName;
56
+ /** Renders `moduleName` with the quote style of an existing specifier. */
57
+ const quoteLike = (specifier, moduleName) => {
58
+ const quote = specifier.getText().startsWith("'") ? "'" : '"';
59
+ return `${quote}${moduleName}${quote}`;
60
+ };
61
+ const renderSpecifier = ({ imported, local, isTypeOnly }, withTypePrefix) => `${withTypePrefix && isTypeOnly ? "type " : ""}${imported === local ? imported : `${imported} as ${local}`}`;
62
+ const statementTerminator = (statement) => (statement.getText().endsWith(";") ? ";" : "");
63
+ const lineOf = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
64
+ /** `true` when `node` is a property-like key (`a.X`, `{ X: 1 }`, `<C X="" />`) rather than a reference to a binding. */
65
+ const isPropertyKey = (node) => {
66
+ const parent = node.parent;
67
+ if (ts.isPropertyAccessExpression(parent) || ts.isQualifiedName(parent)) {
68
+ return ts.isPropertyAccessExpression(parent) ? parent.name === node : parent.right === node;
69
+ }
70
+ if (ts.isBindingElement(parent))
71
+ return parent.propertyName === node;
72
+ if (ts.isPropertyAssignment(parent) ||
73
+ ts.isPropertyDeclaration(parent) ||
74
+ ts.isPropertySignature(parent) ||
75
+ ts.isMethodDeclaration(parent) ||
76
+ ts.isMethodSignature(parent) ||
77
+ ts.isGetAccessorDeclaration(parent) ||
78
+ ts.isSetAccessorDeclaration(parent) ||
79
+ ts.isEnumMember(parent) ||
80
+ ts.isJsxAttribute(parent)) {
81
+ return parent.name === node;
82
+ }
83
+ return false;
84
+ };
85
+ /** `true` when `node` declares a new binding (variable, parameter, function, class, type...). */
86
+ const isBindingDeclaration = (node) => {
87
+ const parent = node.parent;
88
+ if (ts.isBindingElement(parent))
89
+ return parent.name === node;
90
+ if (ts.isVariableDeclaration(parent) ||
91
+ ts.isParameter(parent) ||
92
+ ts.isFunctionDeclaration(parent) ||
93
+ ts.isFunctionExpression(parent) ||
94
+ ts.isClassDeclaration(parent) ||
95
+ ts.isClassExpression(parent) ||
96
+ ts.isInterfaceDeclaration(parent) ||
97
+ ts.isTypeAliasDeclaration(parent) ||
98
+ ts.isEnumDeclaration(parent) ||
99
+ ts.isTypeParameterDeclaration(parent) ||
100
+ ts.isImportSpecifier(parent) ||
101
+ ts.isImportClause(parent) ||
102
+ ts.isNamespaceImport(parent) ||
103
+ ts.isImportEqualsDeclaration(parent)) {
104
+ return parent.name === node;
105
+ }
106
+ return false;
107
+ };
108
+ /** Import and re-export statements are rewritten as a whole, so their identifiers are never edited one by one. */
109
+ const isInsideModuleStatement = (node) => ts.findAncestor(node, ancestor => ts.isImportDeclaration(ancestor) || (ts.isExportDeclaration(ancestor) && ancestor.moduleSpecifier !== undefined)) !== undefined;
110
+ const collectIdentifiers = (sourceFile) => {
111
+ const identifiers = [];
112
+ const visit = (node) => {
113
+ if (ts.isIdentifier(node))
114
+ identifiers.push(node);
115
+ ts.forEachChild(node, visit);
116
+ };
117
+ visit(sourceFile);
118
+ return identifiers;
119
+ };
120
+ /**
121
+ * Edits that rename every reference to the local binding `oldName` to
122
+ * `newName`, or `null` when that can't be done safely (the new name is
123
+ * already used in the file, or `oldName` is redeclared and might be shadowed).
124
+ */
125
+ const renameReferenceEdits = (sourceFile, identifiers, oldName, newName) => {
126
+ if (identifiers.some(id => id.text === newName))
127
+ return null;
128
+ const outside = identifiers.filter(id => id.text === oldName && !isInsideModuleStatement(id));
129
+ if (outside.some(isBindingDeclaration))
130
+ return null;
131
+ const edits = [];
132
+ for (const id of outside) {
133
+ if (isPropertyKey(id))
134
+ continue;
135
+ const start = id.getStart(sourceFile);
136
+ const end = id.getEnd();
137
+ const parent = id.parent;
138
+ if (ts.isShorthandPropertyAssignment(parent)) {
139
+ edits.push({ start, end, text: `${oldName}: ${newName}` });
140
+ }
141
+ else if (ts.isExportSpecifier(parent)) {
142
+ if (parent.propertyName === undefined)
143
+ edits.push({ start, end, text: `${newName} as ${oldName}` });
144
+ else if (parent.propertyName === id)
145
+ edits.push({ start, end, text: newName });
146
+ }
147
+ else {
148
+ edits.push({ start, end, text: newName });
149
+ }
150
+ }
151
+ return edits;
152
+ };
153
+ /**
154
+ * Usages of `fromModule` the codemod can't rewrite statically. They are only
155
+ * reported when the file references a moved name at all, so e.g. a
156
+ * `vi.mock(fromModule, ...)` in a spec that never touches a moved name is not
157
+ * noise. `export *` is always reported: the moved names silently drop out of it.
158
+ */
159
+ const findManualReviewItems = (sourceFile, identifiers, fromModule, toModule, renames) => {
160
+ const items = [];
161
+ const file = sourceFile.fileName;
162
+ const mentionsMovedName = identifiers.some(id => renames.has(id.text)) || sourceFile.text.split(/["'`]/).some(chunk => renames.has(chunk));
163
+ const movedList = [...renames.keys()].join(", ");
164
+ const hint = `moved names (${movedList}) now come from "${toModule}"`;
165
+ const report = (node, what) => {
166
+ items.push({ file, line: lineOf(sourceFile, node), reason: `${what} of "${fromModule}"; ${hint}` });
167
+ };
168
+ const visit = (node) => {
169
+ if (ts.isImportDeclaration(node) && isModule(node.moduleSpecifier, fromModule)) {
170
+ const bindings = node.importClause?.namedBindings;
171
+ if (bindings !== undefined && ts.isNamespaceImport(bindings) && mentionsMovedName) {
172
+ report(node, `namespace import \`* as ${bindings.name.text}\``);
173
+ }
174
+ }
175
+ else if (ts.isExportDeclaration(node) && isModule(node.moduleSpecifier, fromModule)) {
176
+ if (node.exportClause === undefined)
177
+ report(node, "`export *`");
178
+ else if (ts.isNamespaceExport(node.exportClause))
179
+ report(node, "`export * as`");
180
+ }
181
+ else if (ts.isImportEqualsDeclaration(node) && mentionsMovedName) {
182
+ const reference = node.moduleReference;
183
+ if (ts.isExternalModuleReference(reference) && isModule(reference.expression, fromModule)) {
184
+ report(node, "`import = require()`");
185
+ }
186
+ }
187
+ else if (ts.isImportTypeNode(node) && mentionsMovedName) {
188
+ const argument = node.argument;
189
+ if (ts.isLiteralTypeNode(argument) && isModule(argument.literal, fromModule))
190
+ report(node, "`import()` type");
191
+ }
192
+ else if (ts.isCallExpression(node) && mentionsMovedName && isModule(node.arguments[0], fromModule)) {
193
+ const callee = node.expression;
194
+ if (callee.kind === ts.SyntaxKind.ImportKeyword) {
195
+ report(node, "dynamic import()");
196
+ }
197
+ else if (ts.isIdentifier(callee) && callee.text === "require") {
198
+ report(node, "require()");
199
+ }
200
+ else if (ts.isPropertyAccessExpression(callee) &&
201
+ ts.isIdentifier(callee.expression) &&
202
+ MOCK_OBJECTS.has(callee.expression.text) &&
203
+ MOCK_METHODS.has(callee.name.text)) {
204
+ report(node, `${callee.expression.text}.${callee.name.text}()`);
205
+ }
206
+ }
207
+ ts.forEachChild(node, visit);
208
+ };
209
+ visit(sourceFile);
210
+ return items;
211
+ };
212
+ const findTargetImport = (sourceFile, toModule, typeOnly) => {
213
+ for (const statement of sourceFile.statements) {
214
+ if (!ts.isImportDeclaration(statement) || !isModule(statement.moduleSpecifier, toModule))
215
+ continue;
216
+ const clause = statement.importClause;
217
+ if (clause === undefined || clause.isTypeOnly !== typeOnly)
218
+ continue;
219
+ const bindings = clause.namedBindings;
220
+ if (bindings === undefined || !ts.isNamedImports(bindings))
221
+ continue;
222
+ return { statement, elements: bindings.elements, additions: [] };
223
+ }
224
+ return null;
225
+ };
226
+ const specifierKey = ({ imported, local }) => `${imported} as ${local}`;
227
+ const specifierOf = (element, parentTypeOnly) => ({
228
+ imported: (element.propertyName ?? element.name).text,
229
+ local: element.name.text,
230
+ isTypeOnly: parentTypeOnly || element.isTypeOnly,
231
+ });
232
+ const rewriteFile = (filePath, content, { fromModule, toModule }, renames) => {
233
+ const sourceFile = parseSource(filePath, content);
234
+ const identifiers = collectIdentifiers(sourceFile);
235
+ const manualReview = findManualReviewItems(sourceFile, identifiers, fromModule, toModule, renames);
236
+ const edits = [];
237
+ const valueTarget = findTargetImport(sourceFile, toModule, false);
238
+ const typeTarget = findTargetImport(sourceFile, toModule, true);
239
+ const targetFor = (spec) => {
240
+ if (!spec.isTypeOnly)
241
+ return valueTarget;
242
+ return typeTarget ?? valueTarget;
243
+ };
244
+ const replaceStatement = (statement, kept, added) => {
245
+ const parts = [...(kept === null ? [] : [kept]), ...added];
246
+ const start = statement.getStart(sourceFile);
247
+ let end = statement.getEnd();
248
+ if (parts.length === 0) {
249
+ if (content.startsWith("\r\n", end))
250
+ end += 2;
251
+ else if (content.startsWith("\n", end))
252
+ end += 1;
253
+ const leading = content.slice(statement.getFullStart(), start);
254
+ if (leading.includes("eslint-disable")) {
255
+ manualReview.push({
256
+ file: filePath,
257
+ line: lineOf(sourceFile, statement),
258
+ reason: `removed import of "${fromModule}" had an eslint-disable comment, which now applies to the next line`,
259
+ });
260
+ }
261
+ }
262
+ edits.push({ start, end, text: parts.join("\n") });
263
+ };
264
+ const createdImports = new Map();
265
+ const deferredImports = [];
266
+ for (const statement of sourceFile.statements) {
267
+ if (ts.isImportDeclaration(statement) && isModule(statement.moduleSpecifier, fromModule)) {
268
+ const clause = statement.importClause;
269
+ const bindings = clause?.namedBindings;
270
+ if (clause === undefined || bindings === undefined || !ts.isNamedImports(bindings))
271
+ continue;
272
+ const moved = bindings.elements.filter(element => renames.has((element.propertyName ?? element.name).text));
273
+ if (moved.length === 0)
274
+ continue;
275
+ const kept = bindings.elements.filter(element => !moved.includes(element));
276
+ const createdKind = clause.isTypeOnly;
277
+ const addCreated = (spec) => {
278
+ const created = createdImports.get(createdKind) ?? { anchor: statement, specs: [] };
279
+ createdImports.set(createdKind, created);
280
+ if (!created.specs.some(existing => specifierKey(existing) === specifierKey(spec)))
281
+ created.specs.push(spec);
282
+ };
283
+ for (const element of moved) {
284
+ const original = specifierOf(element, clause.isTypeOnly);
285
+ const newName = renames.get(original.imported) ?? original.imported;
286
+ let local = original.local;
287
+ if (newName !== original.imported && original.local === original.imported) {
288
+ const renameEdits = renameReferenceEdits(sourceFile, identifiers, original.local, newName);
289
+ if (renameEdits !== null) {
290
+ edits.push(...renameEdits);
291
+ local = newName;
292
+ }
293
+ }
294
+ const spec = { imported: newName, local, isTypeOnly: original.isTypeOnly };
295
+ const target = targetFor(spec);
296
+ if (target === null) {
297
+ addCreated(spec);
298
+ continue;
299
+ }
300
+ const existing = new Set([
301
+ ...target.elements.map(e => specifierKey(specifierOf(e, false))),
302
+ ...target.additions.map(specifierKey),
303
+ ]);
304
+ if (!existing.has(specifierKey(spec)))
305
+ target.additions.push(spec);
306
+ }
307
+ const terminator = statementTerminator(statement);
308
+ const defaultName = clause.name?.text;
309
+ const keptText = kept.length === 0 && defaultName === undefined
310
+ ? null
311
+ : `import ${clause.isTypeOnly ? "type " : ""}${[
312
+ ...(defaultName === undefined ? [] : [defaultName]),
313
+ ...(kept.length === 0 ? [] : [`{ ${kept.map(e => e.getText(sourceFile)).join(", ")} }`]),
314
+ ].join(", ")} from ${statement.moduleSpecifier.getText(sourceFile)}${terminator}`;
315
+ deferredImports.push({ statement, keptText });
316
+ }
317
+ else if (ts.isExportDeclaration(statement) &&
318
+ isModule(statement.moduleSpecifier, fromModule) &&
319
+ statement.moduleSpecifier !== undefined &&
320
+ statement.exportClause !== undefined &&
321
+ ts.isNamedExports(statement.exportClause)) {
322
+ const elements = statement.exportClause.elements;
323
+ const moved = elements.filter(element => renames.has((element.propertyName ?? element.name).text));
324
+ if (moved.length === 0)
325
+ continue;
326
+ const kept = elements.filter(element => !moved.includes(element));
327
+ const typePrefix = statement.isTypeOnly ? "type " : "";
328
+ const terminator = statementTerminator(statement);
329
+ const specs = moved.map(element => {
330
+ const imported = (element.propertyName ?? element.name).text;
331
+ return renderSpecifier({ imported: renames.get(imported) ?? imported, local: element.name.text, isTypeOnly: element.isTypeOnly }, true);
332
+ });
333
+ replaceStatement(statement, kept.length === 0
334
+ ? null
335
+ : `export ${typePrefix}{ ${kept.map(e => e.getText(sourceFile)).join(", ")} } from ${statement.moduleSpecifier.getText(sourceFile)}${terminator}`, [
336
+ `export ${typePrefix}{ ${specs.join(", ")} } from ${quoteLike(statement.moduleSpecifier, toModule)}${terminator}`,
337
+ ]);
338
+ }
339
+ }
340
+ for (const { statement, keptText } of deferredImports) {
341
+ const createdText = [...createdImports.entries()]
342
+ .filter(([, created]) => created.anchor === statement)
343
+ .map(([typeOnly, created]) => `import ${typeOnly ? "type " : ""}{ ${created.specs
344
+ .map(spec => renderSpecifier(spec, !typeOnly))
345
+ .join(", ")} } from ${quoteLike(statement.moduleSpecifier, toModule)}${statementTerminator(statement)}`);
346
+ replaceStatement(statement, keptText, createdText);
347
+ }
348
+ for (const target of [valueTarget, typeTarget]) {
349
+ if (target === null || target.additions.length === 0)
350
+ continue;
351
+ const typeOnly = target.statement.importClause?.isTypeOnly === true;
352
+ const bindings = target.statement.importClause?.namedBindings;
353
+ if (bindings === undefined)
354
+ continue;
355
+ const merged = [
356
+ ...target.elements.map(e => e.getText(sourceFile)),
357
+ ...target.additions.map(spec => renderSpecifier(spec, !typeOnly)),
358
+ ];
359
+ edits.push({ start: bindings.getStart(sourceFile), end: bindings.getEnd(), text: `{ ${merged.join(", ")} }` });
360
+ }
361
+ if (edits.length === 0)
362
+ return { content, manualReview };
363
+ edits.sort((a, b) => b.start - a.start);
364
+ let updated = content;
365
+ for (const { start, end, text } of edits) {
366
+ updated = updated.slice(0, start) + text + updated.slice(end);
367
+ }
368
+ return { content: updated, manualReview };
369
+ };
370
+ /**
371
+ * Moves named imports and named re-exports of `names` from `fromModule` to
372
+ * `toModule` across every `.ts`/`.tsx`/`.jsx` file in the tree.
373
+ *
374
+ * - Mixed imports are split; names that didn't move stay on `fromModule`.
375
+ * - Aliases (`as`), `import type` and inline `type` modifiers are preserved.
376
+ * - Moved names are merged into an existing named import of `toModule`, or
377
+ * else into one new import per kind (value / `import type`).
378
+ * - With a rename record, an unaliased import has its references renamed too;
379
+ * when that isn't safe (name clash or shadowing) it is imported as
380
+ * `{ NewName as OldName }` instead. Re-exports keep their public name.
381
+ * - Namespace imports, `export *`, dynamic `import()`, `require()` and
382
+ * `vi`/`jest` mocks of `fromModule` are left untouched and returned in
383
+ * `manualReview`, never skipped silently.
384
+ *
385
+ * Synchronous and format-free, so it can run inside a `run-migrations`
386
+ * migration; callers format changed files themselves (e.g. `formatFiles`).
387
+ */
388
+ const moveNamedImports = (tree, options) => {
389
+ const renames = toRenameMap(options.names);
390
+ const changedFiles = [];
391
+ const manualReview = [];
392
+ (0, jsx_utils_1.visitTsFiles)(tree, options.fromModule, (filePath, content) => {
393
+ const result = rewriteFile(filePath, content, options, renames);
394
+ manualReview.push(...result.manualReview);
395
+ if (result.content !== content)
396
+ changedFiles.push(filePath);
397
+ return result.content;
398
+ });
399
+ return { changedFiles, manualReview };
400
+ };
401
+ exports.moveNamedImports = moveNamedImports;
402
+ /** Logs each manual-review item as a warning, prefixed with the migration name. */
403
+ const logManualReview = (migrationName, items) => {
404
+ for (const { file, line, reason } of items) {
405
+ devkit_1.logger.warn(` ${migrationName}: manual review needed at ${file}:${line} - ${reason}`);
406
+ }
407
+ };
408
+ exports.logManualReview = logManualReview;
409
+ //# sourceMappingURL=move-named-imports.js.map
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readCompanionVersion = exports.findPackageJsonsDeclaring = exports.ensureDependency = void 0;
4
+ const devkit_1 = require("@nx/devkit");
5
+ const node_fs_1 = require("node:fs");
6
+ /** Root package name of the manager monorepo, where `@trackunit/*` versions are owned by CI. */
7
+ const MANAGER_WORKSPACE_PACKAGE_NAME = "@trackunit/manager";
8
+ /**
9
+ * Adds `packageName@range` to a package.json unless it is already declared
10
+ * in `dependencies` or `devDependencies` (an existing entry is never
11
+ * changed). Does nothing in the manager monorepo itself. Doesn't install;
12
+ * the consumer has to re-install afterwards.
13
+ */
14
+ const ensureDependency = (tree, options) => {
15
+ const { packageName, range, besidePackage, packageJsonPath = "package.json" } = options;
16
+ if (!tree.exists(packageJsonPath))
17
+ return "no-package-json";
18
+ const packageJson = (0, devkit_1.readJson)(tree, packageJsonPath);
19
+ if (packageJson.name === MANAGER_WORKSPACE_PACKAGE_NAME)
20
+ return "manager-workspace";
21
+ if (packageJson.dependencies?.[packageName] !== undefined ||
22
+ packageJson.devDependencies?.[packageName] !== undefined) {
23
+ return "already-present";
24
+ }
25
+ const asDevDependency = besidePackage !== undefined &&
26
+ packageJson.dependencies?.[besidePackage] === undefined &&
27
+ packageJson.devDependencies?.[besidePackage] !== undefined;
28
+ const entry = { [packageName]: range };
29
+ (0, devkit_1.addDependenciesToPackageJson)(tree, asDevDependency ? {} : entry, asDevDependency ? entry : {}, packageJsonPath, true);
30
+ return "added";
31
+ };
32
+ exports.ensureDependency = ensureDependency;
33
+ /**
34
+ * Every package.json except the root one (and anything under
35
+ * `node_modules`) that declares `packageName` as a dependency, devDependency
36
+ * or peerDependency.
37
+ */
38
+ const findPackageJsonsDeclaring = (tree, packageName) => {
39
+ const found = [];
40
+ (0, devkit_1.visitNotIgnoredFiles)(tree, "/", filePath => {
41
+ if (filePath === "package.json" || !filePath.endsWith("/package.json"))
42
+ return;
43
+ if (filePath.startsWith("node_modules/") || filePath.includes("/node_modules/"))
44
+ return;
45
+ const packageJson = (0, devkit_1.readJson)(tree, filePath);
46
+ if (packageJson.dependencies?.[packageName] !== undefined ||
47
+ packageJson.devDependencies?.[packageName] !== undefined ||
48
+ packageJson.peerDependencies?.[packageName] !== undefined) {
49
+ found.push(filePath);
50
+ }
51
+ });
52
+ return found;
53
+ };
54
+ exports.findPackageJsonsDeclaring = findPackageJsonsDeclaring;
55
+ /**
56
+ * Version of a companion package, read from the first of `candidatePaths`
57
+ * (absolute file-system paths, not tree paths) that is the package.json of
58
+ * `packageName`. Throws when none is, so a migration fails and stays pending
59
+ * instead of writing a wrong range.
60
+ */
61
+ const readCompanionVersion = (packageName, candidatePaths) => {
62
+ for (const candidatePath of candidatePaths) {
63
+ if (!(0, node_fs_1.existsSync)(candidatePath))
64
+ continue;
65
+ const packageJson = JSON.parse((0, node_fs_1.readFileSync)(candidatePath, "utf-8"));
66
+ if (packageJson.name === packageName && typeof packageJson.version === "string" && packageJson.version !== "") {
67
+ return packageJson.version;
68
+ }
69
+ }
70
+ throw new Error(`Could not find the version of ${packageName}. Looked in: ${candidatePaths.join(", ")}`);
71
+ };
72
+ exports.readCompanionVersion = readCompanionVersion;
73
+ //# sourceMappingURL=package-json-utils.js.map
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.moveToRouterComponents = exports.resolveRouterComponentsRange = exports.ROUTER_COMPONENTS_MOVED_NAMES = void 0;
4
+ const devkit_1 = require("@nx/devkit");
5
+ const node_path_1 = require("node:path");
6
+ const jsx_utils_1 = require("../utils/jsx-utils");
7
+ const move_named_imports_1 = require("../utils/move-named-imports");
8
+ const package_json_utils_1 = require("../utils/package-json-utils");
9
+ const MIGRATION_NAME = "move-to-router-components";
10
+ const FROM_MODULE = "@trackunit/react-components";
11
+ const TO_MODULE = "@trackunit/react-router-components";
12
+ /** Exports that left `@trackunit/react-components` for `@trackunit/react-router-components`. */
13
+ exports.ROUTER_COMPONENTS_MOVED_NAMES = [
14
+ "usePersistedState",
15
+ "useSearchParamSync",
16
+ "useCursorUrlSync",
17
+ "MAX_URL_LENGTH",
18
+ "UrlPersistenceLocation",
19
+ "Breadcrumb",
20
+ ];
21
+ /**
22
+ * `^<version>` of `@trackunit/react-router-components`. The published
23
+ * package ships its package.json as a `build-migrations` asset; running from
24
+ * source falls back to the workspace copy.
25
+ */
26
+ const resolveRouterComponentsRange = () => `^${(0, package_json_utils_1.readCompanionVersion)(TO_MODULE, [
27
+ (0, node_path_1.join)(__dirname, "../assets/react-router-components/package.json"),
28
+ (0, node_path_1.join)(__dirname, "../../../router-components/package.json"),
29
+ ])}`;
30
+ exports.resolveRouterComponentsRange = resolveRouterComponentsRange;
31
+ /**
32
+ * Moves imports and re-exports of the router-coupled hooks, constants and components from
33
+ * `@trackunit/react-components` to `@trackunit/react-router-components`.
34
+ *
35
+ * When something was rewritten, `@trackunit/react-router-components` is added
36
+ * to the root package.json next to `@trackunit/react-components` (an
37
+ * existing entry is kept). Per-project package.json files are only reported.
38
+ * Throws before touching any file when the router-components version can't be
39
+ * resolved, so a failed run leaves nothing half-migrated.
40
+ */
41
+ const moveToRouterComponents = (tree, resolveRange = exports.resolveRouterComponentsRange) => {
42
+ const range = resolveRange();
43
+ const { changedFiles, manualReview } = (0, move_named_imports_1.moveNamedImports)(tree, {
44
+ fromModule: FROM_MODULE,
45
+ toModule: TO_MODULE,
46
+ names: exports.ROUTER_COMPONENTS_MOVED_NAMES,
47
+ });
48
+ (0, move_named_imports_1.logManualReview)(MIGRATION_NAME, manualReview);
49
+ if (changedFiles.length > 0) {
50
+ const result = (0, package_json_utils_1.ensureDependency)(tree, {
51
+ packageName: TO_MODULE,
52
+ range,
53
+ besidePackage: FROM_MODULE,
54
+ });
55
+ if (result === "added") {
56
+ devkit_1.logger.info(` ${MIGRATION_NAME}: added ${TO_MODULE} to package.json; re-install dependencies (e.g. \`yarn\` or \`npm install\`) after running migrations`);
57
+ }
58
+ else if (result === "no-package-json") {
59
+ devkit_1.logger.warn(` ${MIGRATION_NAME}: no root package.json found; add ${TO_MODULE} as a dependency yourself`);
60
+ }
61
+ if (result !== "manager-workspace") {
62
+ for (const packageJsonPath of (0, package_json_utils_1.findPackageJsonsDeclaring)(tree, FROM_MODULE)) {
63
+ devkit_1.logger.warn(` ${MIGRATION_NAME}: ${packageJsonPath} declares ${FROM_MODULE}; add ${TO_MODULE} there too if that project uses the moved names`);
64
+ }
65
+ }
66
+ }
67
+ (0, jsx_utils_1.logSummary)(MIGRATION_NAME, changedFiles.length, manualReview.length);
68
+ };
69
+ exports.moveToRouterComponents = moveToRouterComponents;
70
+ exports.default = exports.moveToRouterComponents;
71
+ //# sourceMappingURL=move-to-router-components.js.map
package/migrations.json CHANGED
@@ -49,6 +49,11 @@
49
49
  "version": "3.0.0",
50
50
  "description": "Replace the removed CopyableText with the Text + CopyButton pair (the displayed value stays plain text; only the button copies).",
51
51
  "implementation": "./migrations/v3-0-0/copyabletext-to-text-copybutton"
52
+ },
53
+ "v4-0-0-move-to-router-components": {
54
+ "version": "4.0.0",
55
+ "description": "Move imports of usePersistedState, useSearchParamSync, useCursorUrlSync, MAX_URL_LENGTH, UrlPersistenceLocation and Breadcrumb from @trackunit/react-components to @trackunit/react-router-components, and add @trackunit/react-router-components to the root package.json. Re-install dependencies afterwards.",
56
+ "implementation": "./migrations/v4-0-0/move-to-router-components"
52
57
  }
53
58
  }
54
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "3.12.4-alpha-10f2d9d2ab3.0",
3
+ "version": "4.0.0",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "migrations": "./migrations.json",
@@ -14,16 +14,15 @@
14
14
  "@floating-ui/react": "^0.26.25",
15
15
  "string-ts": "^2.0.0",
16
16
  "tailwind-merge": "^3.7.0",
17
- "@trackunit/ui-design-tokens": "1.21.5-alpha-10f2d9d2ab3.0",
18
- "@trackunit/css-class-variance-utilities": "2.6.5-alpha-10f2d9d2ab3.0",
19
- "@trackunit/shared-utils": "1.22.5-alpha-10f2d9d2ab3.0",
20
- "@trackunit/ui-icons": "1.20.5-alpha-10f2d9d2ab3.0",
17
+ "@trackunit/ui-design-tokens": "1.21.7",
18
+ "@trackunit/css-class-variance-utilities": "2.6.7",
19
+ "@trackunit/shared-utils": "1.22.7",
20
+ "@trackunit/ui-icons": "1.20.7",
21
21
  "es-toolkit": "^1.52.0",
22
22
  "@tanstack/react-virtual": "^3.14.13",
23
- "dequal": "^2.0.3",
24
23
  "fflate": "^0.8.3",
25
24
  "zod": "^3.25.76",
26
- "@trackunit/i18n-library-translation": "2.12.4-alpha-10f2d9d2ab3.0"
25
+ "@trackunit/i18n-library-translation": "2.12.6"
27
26
  },
28
27
  "peerDependencies": {
29
28
  "react": "^19.0.0",
@@ -1,7 +1,7 @@
1
1
  import { MouseEventHandler, ReactElement, ReactNode } from "react";
2
2
  import { CommonProps } from "../../common/CommonProps";
3
- import type { Styleable } from "../../common/Styleable";
4
3
  import { Refable } from "../../common/Refable";
4
+ import type { Styleable } from "../../common/Styleable";
5
5
  export interface ExternalLinkProps extends CommonProps, Styleable, Refable<HTMLAnchorElement> {
6
6
  /**
7
7
  * The URL to which users are redirected
@@ -37,7 +37,7 @@ export interface ExternalLinkProps extends CommonProps, Styleable, Refable<HTMLA
37
37
  * Use ExternalLink for any links that navigate to external resources outside the application (e.g., documentation, support pages, vendor sites).
38
38
  *
39
39
  * ### When not to use
40
- * Do not use ExternalLink for in-app navigation. Use `Link` from `@tanstack/react-router` for internal routes.
40
+ * Do not use ExternalLink for in-app navigation. Use `RouteLink` from `@trackunit/react-router-components` for in-app routes.
41
41
  *
42
42
  * @example Basic external link
43
43
  * ```tsx
package/src/index.d.ts CHANGED
@@ -10,7 +10,6 @@ export * from "./components/Alert/Alert";
10
10
  export * from "./components/AssetIdentity/IdentityCard";
11
11
  export * from "./components/AssetIdentity/IdentityCardSkeleton";
12
12
  export * from "./components/Badge/Badge";
13
- export * from "./components/Breadcrumb/Breadcrumb";
14
13
  export * from "./components/buttons/Button/Button";
15
14
  export * from "./components/buttons/IconButton/IconButton";
16
15
  export * from "./components/buttons/shared/Button.variants";
@@ -126,8 +125,6 @@ export * from "./hooks/localStorage/useLocalStorageReducer";
126
125
  export * from "./hooks/localStorage/useSessionStorage";
127
126
  export * from "./hooks/localStorage/useSessionStorageReducer";
128
127
  export * from "./hooks/noPagination";
129
- export * from "./hooks/persistence/usePersistedState";
130
- export * from "./hooks/persistence/useSearchParamSync";
131
128
  export * from "./hooks/persistence/useStorageKey";
132
129
  export * from "./hooks/persistence/useStripForeignIdentityHash";
133
130
  export * from "./hooks/useBidirectionalScroll";
@@ -135,7 +132,6 @@ export * from "./hooks/useClickOutside";
135
132
  export * from "./hooks/useContainerBreakpoints";
136
133
  export * from "./hooks/useContinuousTimeout";
137
134
  export * from "./hooks/useCopyToClipboard";
138
- export * from "./hooks/useCursorUrlSync";
139
135
  export * from "./hooks/useDebounce";
140
136
  export * from "./hooks/useDevicePixelRatio";
141
137
  export * from "./hooks/useElevatedReducer";