@mk-kit/ui 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,12 @@ ng add @mk-kit/ui
23
23
  ```
24
24
 
25
25
  `ng add` installs the package and wires the theme stylesheet into
26
- `angular.json`. By hand instead:
26
+ `angular.json`.
27
+
28
+ **Coming from PrimeNG?** `ng g @mk-kit/ui:migrate-primeng --dry-run` shows
29
+ what the codemod would rewrite (imports, class names, 1:1 selectors, a few
30
+ input names) and what needs a manual touch; without `--dry-run` it applies
31
+ the changes and writes `primeng-migration.md`. Details: <https://mk-kit.dev/migration>. By hand instead:
27
32
 
28
33
  ```bash
29
34
  npm install @mk-kit/ui
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mk-kit/ui",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -5,6 +5,11 @@
5
5
  "description": "Adds @mk-kit/ui to an application: wires the theme stylesheet into angular.json and optionally scaffolds a provideMkI18n override block.",
6
6
  "factory": "./ng-add/index#ngAdd",
7
7
  "schema": "./ng-add/schema.json"
8
+ },
9
+ "migrate-primeng": {
10
+ "description": "Migrates a PrimeNG app to @mk-kit/ui: rewrites imports, class names and 1:1 selectors, annotates what needs a manual touch, cleans angular.json / package.json and writes a report.",
11
+ "factory": "./migrate-primeng/index#migratePrimeng",
12
+ "schema": "./migrate-primeng/schema.json"
8
13
  }
9
14
  }
10
15
  }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.migratePrimeng = migratePrimeng;
4
+ const mapping_1 = require("./mapping");
5
+ const transform_1 = require("./transform");
6
+ const SKIP_DIRS = new Set(['node_modules', 'dist', '.angular', '.git']);
7
+ function migratePrimeng(options) {
8
+ return (tree, context) => {
9
+ const root = normalize(options.path ?? 'src');
10
+ const dryRun = !!options.dryRun;
11
+ const reportPath = options.report ?? 'primeng-migration.md';
12
+ const files = [];
13
+ let scanned = 0;
14
+ tree.getDir(root).visit((path) => {
15
+ if (path.split('/').some((seg) => SKIP_DIRS.has(seg)))
16
+ return;
17
+ const isTs = path.endsWith('.ts') && !path.endsWith('.d.ts');
18
+ const isHtml = path.endsWith('.html');
19
+ if (!isTs && !isHtml)
20
+ return;
21
+ scanned++;
22
+ const source = tree.read(path)?.toString('utf8');
23
+ if (source === undefined || !/primeng|<p-[a-zA-Z]|\bp[A-Z][a-zA-Z]+\b/.test(source))
24
+ return;
25
+ const result = isTs ? (0, transform_1.transformTypeScript)(source) : (0, transform_1.transformTemplate)(source);
26
+ if (!result.findings.length && !result.changed)
27
+ return;
28
+ files.push({ path, findings: result.findings, changed: result.changed });
29
+ if (result.changed && !dryRun)
30
+ tree.overwrite(path, result.text);
31
+ });
32
+ const unmapped = files.some((f) => f.findings.some((x) => x.kind === 'unmapped'));
33
+ for (const [path, fn] of [
34
+ ['/angular.json', transform_1.transformAngularJson],
35
+ ['/package.json', (src) => (0, transform_1.transformPackageJson)(src, unmapped ? [] : mapping_1.PRIMENG_PACKAGES)],
36
+ ]) {
37
+ if (!tree.exists(path))
38
+ continue;
39
+ scanned++;
40
+ const result = fn(tree.read(path).toString('utf8'));
41
+ if (!result.findings.length)
42
+ continue;
43
+ files.push({ path, findings: result.findings, changed: result.changed });
44
+ if (result.changed && !dryRun)
45
+ tree.overwrite(path, result.text);
46
+ }
47
+ if (unmapped) {
48
+ files.push({
49
+ path: '/package.json',
50
+ changed: false,
51
+ findings: [{ rule: 'package:keep', message: 'primeng kept in package.json: some components have no mk-kit equivalent yet (see "Not available")', kind: 'manual', count: 1 }],
52
+ });
53
+ }
54
+ const report = (0, transform_1.renderReport)(files, { dryRun, scanned });
55
+ if (!dryRun) {
56
+ if (tree.exists(reportPath))
57
+ tree.overwrite(reportPath, report);
58
+ else
59
+ tree.create(reportPath, report);
60
+ }
61
+ const counts = { rewrite: 0, manual: 0, unmapped: 0 };
62
+ for (const f of files)
63
+ for (const x of f.findings)
64
+ counts[x.kind] += x.count;
65
+ context.logger.info(`migrate-primeng: ${dryRun ? 'dry run — ' : ''}${files.filter((f) => f.changed).length} file(s) ${dryRun ? 'would change' : 'changed'}; ` +
66
+ `${counts.rewrite} rewrites, ${counts.manual} manual steps, ${counts.unmapped} without an equivalent.` +
67
+ (dryRun ? '' : ` Report: ${reportPath}`));
68
+ if (dryRun)
69
+ context.logger.info('\n' + report);
70
+ return tree;
71
+ };
72
+ }
73
+ function normalize(p) {
74
+ const trimmed = p.replace(/\/+$/, '');
75
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
76
+ }
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+ /**
3
+ * PrimeNG → @mk-kit/ui mapping used by the `migrate-primeng` schematic.
4
+ *
5
+ * Two tables:
6
+ * - `MODULES`: what each `primeng/<x>` import path exports and which mk-kit
7
+ * class replaces each symbol (`null` = no drop-in equivalent → reported).
8
+ * - `SELECTORS`: template-level rewrites. `to` set + `exact` true means a
9
+ * mechanical rename is safe; otherwise the original markup is left alone
10
+ * and a `<!-- mk-kit: … -->` note is inserted once per file.
11
+ *
12
+ * Everything here is checked against the library's real exports/selectors
13
+ * (see projects/docs/public/api.json); keep it that way when editing.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.PRIMENG_PACKAGES = exports.DOCS_BASE = exports.SELECTORS = exports.MODULES = void 0;
17
+ const D = 'https://mk-kit.dev';
18
+ exports.MODULES = [
19
+ { path: 'button', symbols: { ButtonModule: 'MkButton', Button: 'MkButton', ButtonDirective: 'MkButton' }, docs: '/components/buttons' },
20
+ { path: 'splitbutton', symbols: { SplitButtonModule: 'MkSplitButton', SplitButton: 'MkSplitButton' }, note: 'mk-split-button takes an mk-menu via [menu] instead of [model].', docs: '/components/buttons' },
21
+ { path: 'inputtext', symbols: { InputTextModule: 'MkInput', InputText: 'MkInput' }, docs: '/components/text-inputs' },
22
+ { path: 'textarea', symbols: { TextareaModule: 'MkInput', Textarea: 'MkInput' }, docs: '/components/text-inputs' },
23
+ { path: 'inputtextarea', symbols: { InputTextareaModule: 'MkInput', InputTextarea: 'MkInput' }, docs: '/components/text-inputs' },
24
+ { path: 'inputnumber', symbols: { InputNumberModule: 'MkNumberInput', InputNumber: 'MkNumberInput' }, docs: '/components/text-inputs' },
25
+ { path: 'password', symbols: { PasswordModule: 'MkPasswordInput', Password: 'MkPasswordInput' }, docs: '/components/text-inputs' },
26
+ { path: 'inputotp', symbols: { InputOtpModule: 'MkOtp', InputOtp: 'MkOtp' }, docs: '/components/text-inputs' },
27
+ { path: 'checkbox', symbols: { CheckboxModule: 'MkCheckbox', Checkbox: 'MkCheckbox' }, docs: '/components/toggles' },
28
+ { path: 'radiobutton', symbols: { RadioButtonModule: 'MkRadioGroup', RadioButton: 'MkRadioGroup' }, note: 'mk-radio-group wraps mk-radio children; the group owns the value.', docs: '/components/toggles' },
29
+ { path: 'toggleswitch', symbols: { ToggleSwitchModule: 'MkSwitch', ToggleSwitch: 'MkSwitch' }, docs: '/components/toggles' },
30
+ { path: 'inputswitch', symbols: { InputSwitchModule: 'MkSwitch', InputSwitch: 'MkSwitch' }, docs: '/components/toggles' },
31
+ { path: 'select', symbols: { SelectModule: 'MkSelect', Select: 'MkSelect' }, note: 'mk-select takes [options] as {label, value}[] — no optionLabel/optionValue.', docs: '/components/forms' },
32
+ { path: 'dropdown', symbols: { DropdownModule: 'MkSelect', Dropdown: 'MkSelect' }, note: 'mk-select takes [options] as {label, value}[] — no optionLabel/optionValue.', docs: '/components/forms' },
33
+ { path: 'multiselect', symbols: { MultiSelectModule: 'MkMultiSelect', MultiSelect: 'MkMultiSelect' }, note: 'mk-multi-select takes [options] as {label, value}[].', docs: '/components/selection' },
34
+ { path: 'listbox', symbols: { ListboxModule: 'MkListbox', Listbox: 'MkListbox' }, docs: '/components/selection' },
35
+ { path: 'autocomplete', symbols: { AutoCompleteModule: 'MkAutocomplete', AutoComplete: 'MkAutocomplete' }, note: 'Pass [options] (sync or async) instead of (completeMethod) + [suggestions].', docs: '/components/selection' },
36
+ { path: 'cascadeselect', symbols: { CascadeSelectModule: 'MkCascader', CascadeSelect: 'MkCascader' }, docs: '/components/selection' },
37
+ { path: 'treeselect', symbols: { TreeSelectModule: 'MkTreeSelect', TreeSelect: 'MkTreeSelect' }, docs: '/components/selection' },
38
+ { path: 'selectbutton', symbols: { SelectButtonModule: 'MkButtonToggleGroup', SelectButton: 'MkButtonToggleGroup' }, note: 'mk-button-toggle-group uses mk-button-toggle children instead of [options].', docs: '/components/selection' },
39
+ { path: 'togglebutton', symbols: { ToggleButtonModule: 'MkButtonToggleGroup', ToggleButton: 'MkButtonToggleGroup' }, docs: '/components/selection' },
40
+ { path: 'datepicker', symbols: { DatePickerModule: 'MkDatePicker', DatePicker: 'MkDatePicker' }, docs: '/components/date-time' },
41
+ { path: 'calendar', symbols: { CalendarModule: 'MkDatePicker', Calendar: 'MkDatePicker' }, docs: '/components/date-time' },
42
+ { path: 'rating', symbols: { RatingModule: 'MkRating', Rating: 'MkRating' }, docs: '/components/sliders' },
43
+ { path: 'slider', symbols: { SliderModule: 'MkSlider', Slider: 'MkSlider' }, note: 'Range mode → mk-range-slider.', docs: '/components/sliders' },
44
+ { path: 'colorpicker', symbols: { ColorPickerModule: 'MkColorPicker', ColorPicker: 'MkColorPicker' }, docs: '/components/sliders' },
45
+ { path: 'fileupload', symbols: { FileUploadModule: 'MkFileUpload', FileUpload: 'MkFileUpload' }, note: 'Replace [url] + (onUpload) with [uploadFn].', docs: '/components/forms' },
46
+ { path: 'editor', symbols: { EditorModule: 'MkRichText', Editor: 'MkRichText' }, docs: '/components/rich-text' },
47
+ { path: 'knob', symbols: { KnobModule: 'MkProgressRing', Knob: 'MkProgressRing' }, note: 'mk-progress-ring is display-only; for an editable dial use mk-slider.', docs: '/components/proportion-charts' },
48
+ { path: 'table', symbols: { TableModule: 'MkTable', Table: 'MkTable' }, note: 'mk-table renders from [columns] + [data]; templates via mkTableCell. Server-side paging/sorting → MkTableDataSource.', docs: '/components/table' },
49
+ { path: 'paginator', symbols: { PaginatorModule: 'MkPagination', Paginator: 'MkPagination' }, note: 'mk-pagination is 1-based with [total] + [pageSize]; no [first]/[rows].', docs: '/components/navigation' },
50
+ { path: 'tree', symbols: { TreeModule: 'MkTree', Tree: 'MkTree' }, note: 'Node shape is {label, children, key}; use [nodes].', docs: '/components/tree' },
51
+ { path: 'treetable', symbols: { TreeTableModule: 'MkTable', TreeTable: 'MkTable' }, note: 'mk-table with [childrenKey] renders tree rows.', docs: '/components/table' },
52
+ { path: 'dataview', symbols: { DataViewModule: null, DataView: null }, note: 'No data-view: compose mk-list / mk-grid with @for.', docs: '/components/cards-lists' },
53
+ { path: 'orderlist', symbols: { OrderListModule: 'MkSortableList', OrderList: 'MkSortableList' }, docs: '/components/drag-drop' },
54
+ { path: 'picklist', symbols: { PickListModule: 'MkTransferList', PickList: 'MkTransferList' }, docs: '/components/selection' },
55
+ { path: 'scroller', symbols: { ScrollerModule: 'MkVirtualScroll', Scroller: 'MkVirtualScroll' }, docs: '/components/data' },
56
+ { path: 'virtualscroller', symbols: { VirtualScrollerModule: 'MkVirtualScroll', VirtualScroller: 'MkVirtualScroll' }, docs: '/components/data' },
57
+ { path: 'timeline', symbols: { TimelineModule: 'MkTimeline', Timeline: 'MkTimeline' }, note: 'Use mk-timeline-item children instead of [value] + templates.', docs: '/components/empty-timeline' },
58
+ { path: 'organizationchart', symbols: { OrganizationChartModule: null, OrganizationChart: null }, note: 'No org chart yet.', docs: '/components/tree' },
59
+ { path: 'chart', symbols: { ChartModule: null, UIChart: null }, note: 'p-chart wraps Chart.js. mk-kit ships SVG charts: mk-line-chart, mk-bar-chart, mk-donut-chart, mk-gauge, … — map by [type].', docs: '/components/charts' },
60
+ { path: 'dialog', symbols: { DialogModule: 'MkDialog', Dialog: 'MkDialog' }, note: 'mk-dialog is opened through MkDialogService.open(Component) — no [(visible)] toggle. Move the dialog body into its own component.', docs: '/components/dialogs' },
61
+ { path: 'dynamicdialog', symbols: { DynamicDialogModule: null, DialogService: 'MkDialogService', DynamicDialogRef: 'MkOverlayRef', DynamicDialogConfig: null }, note: 'MkDialogService.open(Cmp, { data }); inject MK_OVERLAY_DATA in the component; await ref.afterClosed.', docs: '/components/dialogs' },
62
+ { path: 'confirmdialog', symbols: { ConfirmDialogModule: null, ConfirmDialog: null }, note: 'Use MkDialogService.confirm({ title, message, tone }) — no <p-confirmDialog> host element needed.', docs: '/components/dialogs' },
63
+ { path: 'confirmpopup', symbols: { ConfirmPopupModule: 'MkPopconfirm', ConfirmPopup: 'MkPopconfirm' }, note: 'Use [mkPopconfirmFor] on the trigger.', docs: '/components/popovers' },
64
+ { path: 'toast', symbols: { ToastModule: null, Toast: null }, note: 'No <p-toast> host: inject MkToastService and call success/info/warning/danger(message, { title }).', docs: '/components/snackbar' },
65
+ { path: 'message', symbols: { MessageModule: 'MkAlert', Message: 'MkAlert' }, docs: '/components/feedback' },
66
+ { path: 'messages', symbols: { MessagesModule: 'MkAlert', Messages: 'MkAlert' }, note: 'One mk-alert per message.', docs: '/components/feedback' },
67
+ { path: 'tooltip', symbols: { TooltipModule: 'MkTooltip', Tooltip: 'MkTooltip' }, docs: '/components/popovers' },
68
+ { path: 'popover', symbols: { PopoverModule: 'MkPopover', Popover: 'MkPopover' }, note: 'Use [mkPopoverTriggerFor] on the trigger instead of #op.toggle($event).', docs: '/components/popovers' },
69
+ { path: 'overlaypanel', symbols: { OverlayPanelModule: 'MkPopover', OverlayPanel: 'MkPopover' }, note: 'Use [mkPopoverTriggerFor] on the trigger instead of #op.toggle($event).', docs: '/components/popovers' },
70
+ { path: 'drawer', symbols: { DrawerModule: 'MkDrawer', Drawer: 'MkDrawer' }, docs: '/components/drawer' },
71
+ { path: 'sidebar', symbols: { SidebarModule: 'MkDrawer', Sidebar: 'MkDrawer' }, docs: '/components/drawer' },
72
+ { path: 'blockui', symbols: { BlockUIModule: 'MkBlockUi', BlockUI: 'MkBlockUi' }, note: 'Use the [mkBlockUi] directive on the region.', docs: '/components/feedback' },
73
+ { path: 'progressbar', symbols: { ProgressBarModule: 'MkProgressBar', ProgressBar: 'MkProgressBar' }, docs: '/components/loading' },
74
+ { path: 'progressspinner', symbols: { ProgressSpinnerModule: 'MkSpinner', ProgressSpinner: 'MkSpinner' }, docs: '/components/loading' },
75
+ { path: 'skeleton', symbols: { SkeletonModule: 'MkSkeleton', Skeleton: 'MkSkeleton' }, docs: '/components/loading' },
76
+ { path: 'badge', symbols: { BadgeModule: 'MkBadge', Badge: 'MkBadge', BadgeDirective: 'MkBadge' }, note: 'pBadge attribute → <mk-badge> element next to the target.', docs: '/components/badges-avatars' },
77
+ { path: 'tag', symbols: { TagModule: 'MkTag', Tag: 'MkTag' }, docs: '/components/badges-avatars' },
78
+ { path: 'chip', symbols: { ChipModule: 'MkChip', Chip: 'MkChip' }, docs: '/components/badges-avatars' },
79
+ { path: 'avatar', symbols: { AvatarModule: 'MkAvatar', Avatar: 'MkAvatar' }, docs: '/components/badges-avatars' },
80
+ { path: 'avatargroup', symbols: { AvatarGroupModule: 'MkAvatarGroup', AvatarGroup: 'MkAvatarGroup' }, docs: '/components/badges-avatars' },
81
+ { path: 'card', symbols: { CardModule: 'MkCard', Card: 'MkCard' }, note: 'Header/footer via mk-card-header / mk-card-footer.', docs: '/components/cards-lists' },
82
+ { path: 'panel', symbols: { PanelModule: 'MkCard', Panel: 'MkCard' }, note: 'Collapsible panel → mk-accordion-item.', docs: '/components/cards-lists' },
83
+ { path: 'fieldset', symbols: { FieldsetModule: 'MkCard', Fieldset: 'MkCard' }, docs: '/components/cards-lists' },
84
+ { path: 'divider', symbols: { DividerModule: 'MkDivider', Divider: 'MkDivider' }, docs: '/components/cards-lists' },
85
+ { path: 'toolbar', symbols: { ToolbarModule: 'MkToolbar', Toolbar: 'MkToolbar' }, docs: '/components/structure' },
86
+ { path: 'splitter', symbols: { SplitterModule: 'MkSplitter', Splitter: 'MkSplitter' }, docs: '/components/structure' },
87
+ { path: 'accordion', symbols: { AccordionModule: 'MkAccordion', Accordion: 'MkAccordion', AccordionPanel: 'MkAccordionItem', AccordionHeader: 'MkAccordionHeader', AccordionContent: null, AccordionTab: 'MkAccordionItem' }, docs: '/components/navigation' },
88
+ { path: 'tabs', symbols: { TabsModule: 'MkTabs', Tabs: 'MkTabs', TabList: null, Tab: 'MkTab', TabPanels: null, TabPanel: null }, note: 'mk-tabs uses <mk-tab label="…"> children: merge each p-tab + p-tabpanel pair.', docs: '/components/navigation' },
89
+ { path: 'tabview', symbols: { TabViewModule: 'MkTabs', TabView: 'MkTabs', TabPanel: 'MkTab' }, docs: '/components/navigation' },
90
+ { path: 'stepper', symbols: { StepperModule: 'MkStepper', Stepper: 'MkStepper', StepList: null, Step: 'MkStep', StepPanels: null, StepPanel: null }, docs: '/components/stepper' },
91
+ { path: 'steps', symbols: { StepsModule: 'MkStepper', Steps: 'MkStepper' }, note: 'Use mk-step children instead of [model].', docs: '/components/stepper' },
92
+ { path: 'menu', symbols: { MenuModule: 'MkMenu', Menu: 'MkMenu' }, note: 'Use mk-menu-item children instead of [model]; open with [mkMenuTriggerFor].', docs: '/components/navigation' },
93
+ { path: 'menubar', symbols: { MenubarModule: 'MkNavList', Menubar: 'MkNavList' }, note: 'Horizontal nav → mk-nav-list / mk-toolbar with mkButton links.', docs: '/components/command-nav' },
94
+ { path: 'tieredmenu', symbols: { TieredMenuModule: 'MkMenu', TieredMenu: 'MkMenu' }, note: 'Nested items via [mkSubmenuFor].', docs: '/components/navigation' },
95
+ { path: 'contextmenu', symbols: { ContextMenuModule: 'MkContextMenuTrigger', ContextMenu: 'MkContextMenuTrigger' }, note: 'Use [mkContextMenuTriggerFor]="menu" on the element.', docs: '/components/context-menu' },
96
+ { path: 'megamenu', symbols: { MegaMenuModule: null, MegaMenu: null }, note: 'No mega menu; compose mk-popover + mk-nav-list.', docs: '/components/command-nav' },
97
+ { path: 'panelmenu', symbols: { PanelMenuModule: 'MkNavList', PanelMenu: 'MkNavList' }, note: 'mk-nav-list with mk-nav-group children.', docs: '/components/command-nav' },
98
+ { path: 'breadcrumb', symbols: { BreadcrumbModule: 'MkBreadcrumb', Breadcrumb: 'MkBreadcrumb' }, note: 'Use mk-breadcrumb-item children instead of [model].', docs: '/components/navigation' },
99
+ { path: 'dock', symbols: { DockModule: null, Dock: null }, note: 'No dock.', docs: '/components/navigation' },
100
+ { path: 'carousel', symbols: { CarouselModule: 'MkCarousel', Carousel: 'MkCarousel' }, note: 'Use mkCarouselSlide templates instead of [value].', docs: '/components/data' },
101
+ { path: 'galleria', symbols: { GalleriaModule: 'MkImageGallery', Galleria: 'MkImageGallery' }, docs: '/components/images' },
102
+ { path: 'image', symbols: { ImageModule: 'MkImage', Image: 'MkImage' }, note: 'Preview → MkLightboxService.', docs: '/components/images' },
103
+ { path: 'scrollpanel', symbols: { ScrollPanelModule: 'MkScrollArea', ScrollPanel: 'MkScrollArea' }, docs: '/components/structure' },
104
+ { path: 'scrolltop', symbols: { ScrollTopModule: 'MkBackToTop', ScrollTop: 'MkBackToTop' }, docs: '/components/navigation' },
105
+ { path: 'speeddial', symbols: { SpeedDialModule: 'MkFab', SpeedDial: 'MkFab' }, note: 'mk-fab with mkFabAction children.', docs: '/components/navigation' },
106
+ { path: 'inplace', symbols: { InplaceModule: 'MkInlineEdit', Inplace: 'MkInlineEdit' }, docs: '/components/table' },
107
+ { path: 'metergroup', symbols: { MeterGroupModule: null, MeterGroup: null }, note: 'No meter group yet; mk-progress-bar per segment.', docs: '/components/loading' },
108
+ { path: 'terminal', symbols: { TerminalModule: null, Terminal: null }, note: 'No terminal.', docs: '/components/data' },
109
+ { path: 'iconfield', symbols: { IconFieldModule: 'MkInputGroup', IconField: 'MkInputGroup', InputIcon: 'MkIcon', InputIconModule: 'MkIcon' }, note: 'mk-input-group with a prefix slot.', docs: '/components/text-inputs' },
110
+ { path: 'inputgroup', symbols: { InputGroupModule: 'MkInputGroup', InputGroup: 'MkInputGroup' }, docs: '/components/text-inputs' },
111
+ { path: 'inputgroupaddon', symbols: { InputGroupAddonModule: null, InputGroupAddon: null }, note: 'Addons are content slots of mk-input-group.', docs: '/components/text-inputs' },
112
+ { path: 'floatlabel', symbols: { FloatLabelModule: 'MkFormField', FloatLabel: 'MkFormField' }, note: 'mk-form-field labelPosition="float".', docs: '/components/forms' },
113
+ { path: 'iftalabel', symbols: { IftaLabelModule: 'MkFormField', IftaLabel: 'MkFormField' }, docs: '/components/forms' },
114
+ { path: 'keyfilter', symbols: { KeyFilterModule: 'MkMask', KeyFilter: 'MkMask' }, note: 'Use [mkMask] patterns or a Validators.pattern.', docs: '/components/utilities' },
115
+ { path: 'inputmask', symbols: { InputMaskModule: 'MkMask', InputMask: 'MkMask' }, note: '[mkMask] on a native input (tokens 0 / A / *).', docs: '/components/utilities' },
116
+ { path: 'ripple', symbols: { RippleModule: 'MkRipple', Ripple: 'MkRipple' }, docs: '/components/utilities' },
117
+ { path: 'focustrap', symbols: { FocusTrapModule: 'MkFocusTrap', FocusTrap: 'MkFocusTrap' }, docs: '/core-services' },
118
+ { path: 'autofocus', symbols: { AutoFocusModule: 'MkAutofocus', AutoFocus: 'MkAutofocus' }, docs: '/components/utilities' },
119
+ { path: 'animateonscroll', symbols: { AnimateOnScrollModule: 'MkIntersect', AnimateOnScroll: 'MkIntersect' }, docs: '/components/utilities' },
120
+ { path: 'styleclass', symbols: { StyleClassModule: null, StyleClass: null }, note: 'Bind [class.x] / @if instead.' },
121
+ { path: 'dragdrop', symbols: { DragDropModule: 'MkDropList', Draggable: 'MkDrag', Droppable: 'MkDropList' }, docs: '/components/drag-drop' },
122
+ { path: 'api', symbols: { MessageService: 'MkToastService', ConfirmationService: 'MkDialogService', MenuItem: null, PrimeNGConfig: null, PrimeIcons: null, FilterService: null, SortEvent: null, TreeNode: null, SelectItem: null, PrimeTemplate: null, SharedModule: null }, note: 'MessageService.add({severity, summary, detail}) → MkToastService.<severity>(detail, { title: summary }). ConfirmationService.confirm({…}) → await MkDialogService.confirm({ title, message, tone }).', docs: '/components/snackbar' },
123
+ { path: 'config', symbols: { PrimeNG: null, providePrimeNG: null }, note: 'providePrimeNG({ theme }) → import @mk-kit/ui/styles.css and use provideMkI18n for strings.', docs: '/theming' },
124
+ ];
125
+ /** Element / attribute rewrites for templates. Order matters only for overlapping prefixes (longest first is enforced by the transform). */
126
+ exports.SELECTORS = [
127
+ // Buttons
128
+ { kind: 'attribute', from: 'pButton', to: 'mkButton', attrs: { severity: 'tone', '[severity]': '[tone]', '[outlined]': null, '[loading]': '[loading]', '[disabled]': '[disabled]' }, note: 'label="…" / icon="…" become content: <button mkButton><mk-icon name="…" /> Label</button>; [outlined]/[text]/[link] → variant="outline" | "ghost" | "link".', manual: true, docs: '/components/buttons' },
129
+ { kind: 'element', from: 'p-button', note: '<p-button label="…"> → <button mkButton>…</button>; severity → tone; [outlined] → variant="outline".', docs: '/components/buttons' },
130
+ { kind: 'element', from: 'p-splitbutton', to: 'mk-split-button', manual: true, note: '[model] → an <mk-menu #m> with mk-menu-item children and [menu]="m"; label → content.', docs: '/components/buttons' },
131
+ { kind: 'element', from: 'p-splitButton', to: 'mk-split-button', manual: true, note: '[model] → an <mk-menu #m> with mk-menu-item children and [menu]="m"; label → content.', docs: '/components/buttons' },
132
+ // Text inputs
133
+ { kind: 'attribute', from: 'pInputText', to: 'mkInput', docs: '/components/text-inputs' },
134
+ { kind: 'attribute', from: 'pTextarea', to: 'mkInput', docs: '/components/text-inputs' },
135
+ { kind: 'attribute', from: 'pInputTextarea', to: 'mkInput', docs: '/components/text-inputs' },
136
+ { kind: 'attribute', from: 'pAutoFocus', to: 'mkAutofocus', docs: '/components/utilities' },
137
+ { kind: 'attribute', from: 'pRipple', to: 'mkRipple', docs: '/components/utilities' },
138
+ { kind: 'attribute', from: 'pTooltip', to: 'mkTooltip', attrs: { tooltipPosition: 'mkTooltipPlacement', '[tooltipPosition]': '[mkTooltipPlacement]' }, docs: '/components/popovers' },
139
+ { kind: 'attribute', from: 'pBadge', note: 'pBadge attribute → place an <mk-badge dot> / <mk-badge>{{ n }}</mk-badge> next to the element.', docs: '/components/badges-avatars' },
140
+ { kind: 'attribute', from: 'pKeyFilter', note: 'Use [mkMask] or Validators.pattern.', docs: '/components/utilities' },
141
+ { kind: 'attribute', from: 'pInputMask', to: 'mkMask', manual: true, note: 'Mask tokens: PrimeNG 9/a/* → mk-kit 0/A/*.', docs: '/components/utilities' },
142
+ { kind: 'element', from: 'p-inputnumber', to: 'mk-number-input', manual: true, note: 'mode="currency" → mk-currency-input; [showButtons] is always on.', docs: '/components/text-inputs' },
143
+ { kind: 'element', from: 'p-inputNumber', to: 'mk-number-input', manual: true, note: 'mode="currency" → mk-currency-input; [showButtons] is always on.', docs: '/components/text-inputs' },
144
+ { kind: 'element', from: 'p-password', to: 'mk-password-input', attrs: { '[feedback]': '[showStrength]', '[toggleMask]': null }, docs: '/components/text-inputs' },
145
+ { kind: 'element', from: 'p-inputotp', to: 'mk-otp', docs: '/components/text-inputs' },
146
+ { kind: 'element', from: 'p-inputOtp', to: 'mk-otp', docs: '/components/text-inputs' },
147
+ { kind: 'element', from: 'p-floatlabel', to: 'mk-form-field', manual: true, note: 'Add labelPosition="float" and move the <label> text into [label].', docs: '/components/forms' },
148
+ { kind: 'element', from: 'p-floatLabel', to: 'mk-form-field', manual: true, note: 'Add labelPosition="float" and move the <label> text into [label].', docs: '/components/forms' },
149
+ { kind: 'element', from: 'p-iconfield', to: 'mk-input-group', manual: true, note: 'Icon → a prefix slot inside mk-input-group.', docs: '/components/text-inputs' },
150
+ { kind: 'element', from: 'p-iconField', to: 'mk-input-group', manual: true, note: 'Icon → a prefix slot inside mk-input-group.', docs: '/components/text-inputs' },
151
+ { kind: 'element', from: 'p-inputgroupaddon', note: 'Addons are content slots of mk-input-group: replace the wrapper with a plain element inside mk-input-group.', docs: '/components/text-inputs' },
152
+ { kind: 'element', from: 'p-inputGroupAddon', note: 'Addons are content slots of mk-input-group: replace the wrapper with a plain element inside mk-input-group.', docs: '/components/text-inputs' },
153
+ { kind: 'element', from: 'p-inputgroup', to: 'mk-input-group', docs: '/components/text-inputs' },
154
+ { kind: 'element', from: 'p-inputGroup', to: 'mk-input-group', docs: '/components/text-inputs' },
155
+ // Toggles & selection
156
+ { kind: 'element', from: 'p-checkbox', to: 'mk-checkbox', manual: true, note: 'Non-binary (array) checkboxes → one mk-checkbox per option; label="…" → content.', docs: '/components/toggles' },
157
+ { kind: 'element', from: 'p-radiobutton', note: 'Wrap options in <mk-radio-group [(ngModel)]> with <mk-radio value="…">Label</mk-radio> children.', docs: '/components/toggles' },
158
+ { kind: 'element', from: 'p-radioButton', note: 'Wrap options in <mk-radio-group [(ngModel)]> with <mk-radio value="…">Label</mk-radio> children.', docs: '/components/toggles' },
159
+ { kind: 'element', from: 'p-toggleswitch', to: 'mk-switch', docs: '/components/toggles' },
160
+ { kind: 'element', from: 'p-toggleSwitch', to: 'mk-switch', docs: '/components/toggles' },
161
+ { kind: 'element', from: 'p-inputswitch', to: 'mk-switch', docs: '/components/toggles' },
162
+ { kind: 'element', from: 'p-inputSwitch', to: 'mk-switch', docs: '/components/toggles' },
163
+ { kind: 'element', from: 'p-select', to: 'mk-select', manual: true, note: 'Map [options] to {label, value}[] (no optionLabel/optionValue); [showClear] not needed.', docs: '/components/forms' },
164
+ { kind: 'element', from: 'p-dropdown', to: 'mk-select', manual: true, note: 'Map [options] to {label, value}[] (no optionLabel/optionValue).', docs: '/components/forms' },
165
+ { kind: 'element', from: 'p-multiselect', to: 'mk-multi-select', manual: true, note: 'Map [options] to {label, value}[].', docs: '/components/selection' },
166
+ { kind: 'element', from: 'p-multiSelect', to: 'mk-multi-select', manual: true, note: 'Map [options] to {label, value}[].', docs: '/components/selection' },
167
+ { kind: 'element', from: 'p-listbox', to: 'mk-listbox', manual: true, note: 'Map [options] to {label, value}[].', docs: '/components/selection' },
168
+ { kind: 'element', from: 'p-autocomplete', to: 'mk-autocomplete', manual: true, note: '(completeMethod) + [suggestions] → [options] (array or (query) => Promise/Observable).', docs: '/components/selection' },
169
+ { kind: 'element', from: 'p-autoComplete', to: 'mk-autocomplete', manual: true, note: '(completeMethod) + [suggestions] → [options] (array or (query) => Promise/Observable).', docs: '/components/selection' },
170
+ { kind: 'element', from: 'p-cascadeselect', to: 'mk-cascader', manual: true, note: 'Options: {label, value, children}[].', docs: '/components/selection' },
171
+ { kind: 'element', from: 'p-cascadeSelect', to: 'mk-cascader', manual: true, note: 'Options: {label, value, children}[].', docs: '/components/selection' },
172
+ { kind: 'element', from: 'p-treeselect', to: 'mk-tree-select', manual: true, note: '[options] → [nodes] ({label, key, children}).', docs: '/components/selection' },
173
+ { kind: 'element', from: 'p-treeSelect', to: 'mk-tree-select', manual: true, note: '[options] → [nodes] ({label, key, children}).', docs: '/components/selection' },
174
+ { kind: 'element', from: 'p-selectbutton', to: 'mk-button-toggle-group', manual: true, note: '[options] → <mk-button-toggle value="…">Label</mk-button-toggle> children.', docs: '/components/selection' },
175
+ { kind: 'element', from: 'p-selectButton', to: 'mk-button-toggle-group', manual: true, note: '[options] → <mk-button-toggle value="…">Label</mk-button-toggle> children.', docs: '/components/selection' },
176
+ { kind: 'element', from: 'p-togglebutton', note: 'Single toggle → <mk-button-toggle-group> with one <mk-button-toggle>, or a checkbox-style mkButton.', docs: '/components/selection' },
177
+ { kind: 'element', from: 'p-toggleButton', note: 'Single toggle → <mk-button-toggle-group> with one <mk-button-toggle>, or a checkbox-style mkButton.', docs: '/components/selection' },
178
+ { kind: 'element', from: 'p-rating', to: 'mk-rating', attrs: { stars: 'max', '[stars]': '[max]' }, docs: '/components/sliders' },
179
+ { kind: 'element', from: 'p-slider', to: 'mk-slider', manual: true, note: '[range] → mk-range-slider with [low, high] value.', docs: '/components/sliders' },
180
+ { kind: 'element', from: 'p-colorpicker', to: 'mk-color-picker', docs: '/components/sliders' },
181
+ { kind: 'element', from: 'p-colorPicker', to: 'mk-color-picker', docs: '/components/sliders' },
182
+ { kind: 'element', from: 'p-knob', note: 'Display → mk-progress-ring [value]; editable → mk-slider.', docs: '/components/proportion-charts' },
183
+ // Dates
184
+ { kind: 'element', from: 'p-datepicker', to: 'mk-date-picker', manual: true, note: '[minDate]/[maxDate] → [min]/[max]; [showTime] → mk-datetime-picker; [selectionMode]="range" → mk-date-range-picker; dateFormat → displayFormat (yyyy-MM-dd tokens).', docs: '/components/date-time' },
185
+ { kind: 'element', from: 'p-datePicker', to: 'mk-date-picker', manual: true, note: '[minDate]/[maxDate] → [min]/[max]; [showTime] → mk-datetime-picker; range → mk-date-range-picker.', docs: '/components/date-time' },
186
+ { kind: 'element', from: 'p-calendar', to: 'mk-date-picker', manual: true, note: '[minDate]/[maxDate] → [min]/[max]; [showTime] → mk-datetime-picker; range → mk-date-range-picker; [timeOnly] → mk-time-picker.', docs: '/components/date-time' },
187
+ // Files & editors
188
+ { kind: 'element', from: 'p-fileupload', to: 'mk-file-upload', manual: true, note: '[url] + (onUpload) → [uploadFn]; [maxFileSize] → [maxSize]; mode="basic" is the default look.', docs: '/components/forms' },
189
+ { kind: 'element', from: 'p-fileUpload', to: 'mk-file-upload', manual: true, note: '[url] + (onUpload) → [uploadFn]; [maxFileSize] → [maxSize].', docs: '/components/forms' },
190
+ { kind: 'element', from: 'p-editor', to: 'mk-rich-text', manual: true, note: 'HTML in/out via [(ngModel)] works; toolbar customisation differs (see docs).', docs: '/components/rich-text' },
191
+ // Data
192
+ { kind: 'element', from: 'p-table', to: 'mk-table', manual: true, note: '[value] → [data]; define [columns] ({key, header, sortable}) and drop the <ng-template pTemplate="header|body"> blocks (custom cells → mkTableCell); [paginator] → mk-pagination or MkTableDataSource; selection → [selectable] + [(selected)].', attrs: { '[value]': '[data]', '[selectionMode]': null, '[rowHover]': '[hover]', '[striped]': '[zebra]', '[stripedRows]': '[zebra]' }, docs: '/components/table' },
193
+ { kind: 'element', from: 'p-treetable', to: 'mk-table', manual: true, note: 'mk-table with [childrenKey]="\'children\'"; same column mapping as p-table.', attrs: { '[value]': '[data]' }, docs: '/components/table' },
194
+ { kind: 'element', from: 'p-treeTable', to: 'mk-table', manual: true, note: 'mk-table with [childrenKey]="\'children\'"; same column mapping as p-table.', attrs: { '[value]': '[data]' }, docs: '/components/table' },
195
+ { kind: 'element', from: 'p-paginator', to: 'mk-pagination', manual: true, note: '[rows] → [pageSize], [totalRecords] → [total], [first] → 1-based [(page)]; (onPageChange) → (pageChange).', attrs: { '[rows]': '[pageSize]', '[totalRecords]': '[total]', '(onPageChange)': '(pageChange)' }, docs: '/components/navigation' },
196
+ { kind: 'element', from: 'p-dataview', note: 'Compose mk-list / mk-grid with @for; mk-pagination for paging.', docs: '/components/cards-lists' },
197
+ { kind: 'element', from: 'p-dataView', note: 'Compose mk-list / mk-grid with @for; mk-pagination for paging.', docs: '/components/cards-lists' },
198
+ { kind: 'element', from: 'p-tree', to: 'mk-tree', manual: true, note: '[value] → [nodes] ({label, key, children}); [(selection)] → [(selected)].', attrs: { '[value]': '[nodes]' }, docs: '/components/tree' },
199
+ { kind: 'element', from: 'p-orderlist', to: 'mk-sortable-list', manual: true, note: '[value] → [(items)] with a projected row template.', docs: '/components/drag-drop' },
200
+ { kind: 'element', from: 'p-orderList', to: 'mk-sortable-list', manual: true, note: '[value] → [(items)] with a projected row template.', docs: '/components/drag-drop' },
201
+ { kind: 'element', from: 'p-picklist', to: 'mk-transfer-list', manual: true, note: '[source]/[target] → [items] + [(value)].', docs: '/components/selection' },
202
+ { kind: 'element', from: 'p-pickList', to: 'mk-transfer-list', manual: true, note: '[source]/[target] → [items] + [(value)].', docs: '/components/selection' },
203
+ { kind: 'element', from: 'p-scroller', to: 'mk-virtual-scroll', manual: true, note: '[itemSize] → [itemHeight]; row template is projected.', attrs: { '[itemSize]': '[itemHeight]' }, docs: '/components/data' },
204
+ { kind: 'element', from: 'p-virtualscroller', to: 'mk-virtual-scroll', manual: true, note: '[itemSize] → [itemHeight]; row template is projected.', attrs: { '[itemSize]': '[itemHeight]' }, docs: '/components/data' },
205
+ { kind: 'element', from: 'p-timeline', to: 'mk-timeline', manual: true, note: '[value] + templates → <mk-timeline-item> children.', docs: '/components/empty-timeline' },
206
+ { kind: 'element', from: 'p-organizationchart', note: 'No org chart yet — mk-tree is the closest.', docs: '/components/tree' },
207
+ { kind: 'element', from: 'p-organizationChart', note: 'No org chart yet — mk-tree is the closest.', docs: '/components/tree' },
208
+ { kind: 'element', from: 'p-chart', note: 'Replace by the matching SVG chart: type="line" → mk-line-chart, "bar" → mk-bar-chart, "doughnut"/"pie" → mk-donut-chart, "radar" → mk-radar-chart, "scatter"/"bubble" → mk-scatter-chart. Data shape is plain series arrays.', docs: '/components/charts' },
209
+ { kind: 'element', from: 'p-metergroup', note: 'No meter group yet; use one mk-progress-bar per segment.', docs: '/components/loading' },
210
+ { kind: 'element', from: 'p-meterGroup', note: 'No meter group yet; use one mk-progress-bar per segment.', docs: '/components/loading' },
211
+ // Overlays & feedback
212
+ { kind: 'element', from: 'p-dialog', note: 'mk-dialog is opened with MkDialogService.open(Component, { data }) — move this <p-dialog> body into a component (see docs) and drop [(visible)].', docs: '/components/dialogs' },
213
+ { kind: 'element', from: 'p-confirmdialog', note: 'Remove this host element; call await MkDialogService.confirm({ title, message, tone }) where you used ConfirmationService.', docs: '/components/dialogs' },
214
+ { kind: 'element', from: 'p-confirmDialog', note: 'Remove this host element; call await MkDialogService.confirm({ title, message, tone }) where you used ConfirmationService.', docs: '/components/dialogs' },
215
+ { kind: 'element', from: 'p-confirmpopup', note: 'Put [mkPopconfirmFor]="pc" on the trigger and an <mk-popconfirm #pc message="…" (confirm)="…"> next to it.', docs: '/components/popovers' },
216
+ { kind: 'element', from: 'p-confirmPopup', note: 'Put [mkPopconfirmFor]="pc" on the trigger and an <mk-popconfirm #pc message="…" (confirm)="…"> next to it.', docs: '/components/popovers' },
217
+ { kind: 'element', from: 'p-toast', note: 'Remove this host element; MkToastService mounts its own container. MessageService.add({severity, summary, detail}) → toast.<severity>(detail, { title: summary }).', docs: '/components/snackbar' },
218
+ { kind: 'element', from: 'p-message', to: 'mk-alert', manual: true, note: 'severity → tone (error → danger); text="…" → content.', attrs: { severity: 'tone', '[severity]': '[tone]' }, docs: '/components/feedback' },
219
+ { kind: 'element', from: 'p-messages', note: 'Render one <mk-alert [tone]> per message with @for.', docs: '/components/feedback' },
220
+ { kind: 'element', from: 'p-popover', note: 'Put [mkPopoverTriggerFor]="pop" on the trigger and rename this element to <mk-popover #pop>; drop #op.toggle($event).', docs: '/components/popovers' },
221
+ { kind: 'element', from: 'p-overlaypanel', note: 'Put [mkPopoverTriggerFor]="pop" on the trigger and rename this element to <mk-popover #pop>; drop #op.toggle($event).', docs: '/components/popovers' },
222
+ { kind: 'element', from: 'p-overlayPanel', note: 'Put [mkPopoverTriggerFor]="pop" on the trigger and rename this element to <mk-popover #pop>; drop #op.toggle($event).', docs: '/components/popovers' },
223
+ { kind: 'element', from: 'p-drawer', to: 'mk-drawer', manual: true, note: '[(visible)] → [(open)]; position → side; [modal] → [hasBackdrop].', attrs: { '[(visible)]': '[(open)]', '[visible]': '[open]', '(visibleChange)': '(openChange)', position: 'side', '[position]': '[side]', '[modal]': '[hasBackdrop]', header: 'heading', '[header]': '[heading]' }, docs: '/components/drawer' },
224
+ { kind: 'element', from: 'p-sidebar', to: 'mk-drawer', manual: true, note: '[(visible)] → [(open)]; position → side; [modal] → [hasBackdrop].', attrs: { '[(visible)]': '[(open)]', '[visible]': '[open]', '(visibleChange)': '(openChange)', position: 'side', '[position]': '[side]', '[modal]': '[hasBackdrop]', header: 'heading', '[header]': '[heading]' }, docs: '/components/drawer' },
225
+ { kind: 'element', from: 'p-blockui', note: 'Use the [mkBlockUi]="busy()" directive on the region (or MkBlockUiService for the page).', docs: '/components/feedback' },
226
+ { kind: 'element', from: 'p-blockUI', note: 'Use the [mkBlockUi]="busy()" directive on the region (or MkBlockUiService for the page).', docs: '/components/feedback' },
227
+ { kind: 'element', from: 'p-progressbar', to: 'mk-progress-bar', attrs: { '[mode]': null, mode: null }, manual: true, note: 'mode="indeterminate" → the indeterminate attribute.', docs: '/components/loading' },
228
+ { kind: 'element', from: 'p-progressBar', to: 'mk-progress-bar', manual: true, note: 'mode="indeterminate" → the indeterminate attribute.', docs: '/components/loading' },
229
+ { kind: 'element', from: 'p-progressspinner', to: 'mk-spinner', docs: '/components/loading' },
230
+ { kind: 'element', from: 'p-progressSpinner', to: 'mk-spinner', docs: '/components/loading' },
231
+ { kind: 'element', from: 'p-skeleton', to: 'mk-skeleton', attrs: { shape: 'shape', width: 'width', height: 'height' }, docs: '/components/loading' },
232
+ { kind: 'element', from: 'p-badge', to: 'mk-badge', manual: true, note: '[value] → content; severity → tone.', attrs: { severity: 'tone', '[severity]': '[tone]' }, docs: '/components/badges-avatars' },
233
+ { kind: 'element', from: 'p-tag', to: 'mk-tag', manual: true, note: '[value] → content; severity → tone; [rounded] is the default look.', attrs: { severity: 'tone', '[severity]': '[tone]' }, docs: '/components/badges-avatars' },
234
+ { kind: 'element', from: 'p-chip', to: 'mk-chip', manual: true, note: 'label="…" → content; [removable] is the same; (onRemove) → (remove).', attrs: { '(onRemove)': '(remove)' }, docs: '/components/badges-avatars' },
235
+ { kind: 'element', from: 'p-avatar', to: 'mk-avatar', manual: true, note: 'label="AB" → name="A B" (initials are derived); image → src; shape="circle" is the default.', attrs: { image: 'src', '[image]': '[src]' }, docs: '/components/badges-avatars' },
236
+ { kind: 'element', from: 'p-avatargroup', to: 'mk-avatar-group', docs: '/components/badges-avatars' },
237
+ { kind: 'element', from: 'p-avatarGroup', to: 'mk-avatar-group', docs: '/components/badges-avatars' },
238
+ { kind: 'element', from: 'p-card', to: 'mk-card', manual: true, note: 'header="…" → <mk-card-header> / <mk-card-title>; pTemplate="footer" → <mk-card-footer>.', docs: '/components/cards-lists' },
239
+ { kind: 'element', from: 'p-panel', to: 'mk-card', manual: true, note: 'header → <mk-card-header>; [toggleable] → use mk-accordion-item instead.', docs: '/components/cards-lists' },
240
+ { kind: 'element', from: 'p-fieldset', to: 'mk-card', manual: true, note: 'legend → <mk-card-header>.', docs: '/components/cards-lists' },
241
+ { kind: 'element', from: 'p-divider', to: 'mk-divider', attrs: { layout: 'orientation', '[layout]': '[orientation]' }, docs: '/components/cards-lists' },
242
+ { kind: 'element', from: 'p-toolbar', to: 'mk-toolbar', manual: true, note: 'pTemplate="start|end" → plain content; use mk-flex for alignment.', docs: '/components/structure' },
243
+ { kind: 'element', from: 'p-splitter', to: 'mk-splitter', manual: true, note: 'p-splitterpanel children → two projected children; [layout] → orientation.', attrs: { layout: 'orientation', '[layout]': '[orientation]' }, docs: '/components/structure' },
244
+ { kind: 'element', from: 'p-splitterpanel', note: 'Drop the wrapper: mk-splitter projects its two children directly.', docs: '/components/structure' },
245
+ { kind: 'element', from: 'p-splitterPanel', note: 'Drop the wrapper: mk-splitter projects its two children directly.', docs: '/components/structure' },
246
+ { kind: 'element', from: 'p-scrollpanel', to: 'mk-scroll-area', docs: '/components/structure' },
247
+ { kind: 'element', from: 'p-scrollPanel', to: 'mk-scroll-area', docs: '/components/structure' },
248
+ { kind: 'element', from: 'p-scrolltop', to: 'mk-back-to-top', docs: '/components/navigation' },
249
+ { kind: 'element', from: 'p-scrollTop', to: 'mk-back-to-top', docs: '/components/navigation' },
250
+ { kind: 'element', from: 'p-image', to: 'mk-image', manual: true, note: '[preview] → MkLightboxService.open().', docs: '/components/images' },
251
+ { kind: 'element', from: 'p-galleria', to: 'mk-image-gallery', manual: true, note: '[value] → [images] ({src, alt}).', docs: '/components/images' },
252
+ { kind: 'element', from: 'p-carousel', to: 'mk-carousel', manual: true, note: '[value] + template → <ng-template mkCarouselSlide> per item.', docs: '/components/data' },
253
+ { kind: 'element', from: 'p-inplace', to: 'mk-inline-edit', manual: true, note: 'Bind [(value)] instead of display/content templates.', docs: '/components/table' },
254
+ // Navigation
255
+ { kind: 'element', from: 'p-tabs', to: 'mk-tabs', manual: true, note: 'Merge each <p-tab> + <p-tabpanel> pair into <mk-tab label="…">…</mk-tab>; drop p-tablist / p-tabpanels.', docs: '/components/navigation' },
256
+ { kind: 'element', from: 'p-tablist', note: 'Drop: mk-tabs renders its own tab list.', docs: '/components/navigation' },
257
+ { kind: 'element', from: 'p-tabpanels', note: 'Drop: <mk-tab> children go directly inside <mk-tabs>.', docs: '/components/navigation' },
258
+ { kind: 'element', from: 'p-tabpanel', note: 'Becomes <mk-tab label="…"> (label from the matching <p-tab>).', docs: '/components/navigation' },
259
+ { kind: 'element', from: 'p-tabview', to: 'mk-tabs', manual: true, note: '[(activeIndex)] → [(selectedIndex)].', attrs: { '[(activeIndex)]': '[(selectedIndex)]', '[activeIndex]': '[selectedIndex]', '(activeIndexChange)': '(selectedIndexChange)' }, docs: '/components/navigation' },
260
+ { kind: 'element', from: 'p-tabView', to: 'mk-tabs', manual: true, note: '[(activeIndex)] → [(selectedIndex)].', attrs: { '[(activeIndex)]': '[(selectedIndex)]', '[activeIndex]': '[selectedIndex]', '(activeIndexChange)': '(selectedIndexChange)' }, docs: '/components/navigation' },
261
+ { kind: 'element', from: 'p-tabPanel', to: 'mk-tab', attrs: { header: 'label', '[header]': '[label]' }, docs: '/components/navigation' },
262
+ { kind: 'element', from: 'p-accordion', to: 'mk-accordion', attrs: { '[multiple]': '[multi]', multiple: 'multi' }, docs: '/components/navigation' },
263
+ { kind: 'element', from: 'p-accordion-panel', to: 'mk-accordion-item', manual: true, note: 'Move the <p-accordion-header> text into header="…" and drop the p-accordion-content wrapper.', docs: '/components/navigation' },
264
+ { kind: 'element', from: 'p-accordionpanel', to: 'mk-accordion-item', manual: true, note: 'Move the <p-accordion-header> text into header="…" and drop the p-accordion-content wrapper.', docs: '/components/navigation' },
265
+ { kind: 'element', from: 'p-accordion-header', note: 'Becomes header="…" on the mk-accordion-item (or an [mkAccordionHeader] template).', docs: '/components/navigation' },
266
+ { kind: 'element', from: 'p-accordion-content', note: 'Drop the wrapper: content goes directly inside mk-accordion-item.', docs: '/components/navigation' },
267
+ { kind: 'element', from: 'p-accordiontab', to: 'mk-accordion-item', docs: '/components/navigation' },
268
+ { kind: 'element', from: 'p-accordionTab', to: 'mk-accordion-item', docs: '/components/navigation' },
269
+ { kind: 'element', from: 'p-stepper', to: 'mk-stepper', manual: true, note: 'Each p-step + p-step-panel → <mk-step label="…">; [(value)] → [(selectedIndex)] (0-based).', docs: '/components/stepper' },
270
+ { kind: 'element', from: 'p-steps', to: 'mk-stepper', manual: true, note: '[model] → <mk-step label="…"> children; [(activeIndex)] → [(selectedIndex)].', attrs: { '[(activeIndex)]': '[(selectedIndex)]', '[activeIndex]': '[selectedIndex]' }, docs: '/components/stepper' },
271
+ { kind: 'element', from: 'p-menu', to: 'mk-menu', manual: true, note: '[model] → <mk-menu-item> children; [popup] + #menu.toggle($event) → [mkMenuTriggerFor]="menu" on the button.', docs: '/components/navigation' },
272
+ { kind: 'element', from: 'p-tieredmenu', to: 'mk-menu', manual: true, note: '[model] → <mk-menu-item> children; nested items via [mkSubmenuFor].', docs: '/components/navigation' },
273
+ { kind: 'element', from: 'p-tieredMenu', to: 'mk-menu', manual: true, note: '[model] → <mk-menu-item> children; nested items via [mkSubmenuFor].', docs: '/components/navigation' },
274
+ { kind: 'element', from: 'p-contextmenu', to: 'mk-menu', manual: true, note: '[model] → <mk-menu-item> children; [target] → put [mkContextMenuTriggerFor]="menu" on the element.', docs: '/components/context-menu' },
275
+ { kind: 'element', from: 'p-contextMenu', to: 'mk-menu', manual: true, note: '[model] → <mk-menu-item> children; [target] → put [mkContextMenuTriggerFor]="menu" on the element.', docs: '/components/context-menu' },
276
+ { kind: 'element', from: 'p-menubar', note: 'Horizontal nav → <mk-toolbar> with mkButton links, or mk-nav-list.', docs: '/components/command-nav' },
277
+ { kind: 'element', from: 'p-megamenu', note: 'No mega menu; compose mk-popover + mk-nav-list.', docs: '/components/command-nav' },
278
+ { kind: 'element', from: 'p-megaMenu', note: 'No mega menu; compose mk-popover + mk-nav-list.', docs: '/components/command-nav' },
279
+ { kind: 'element', from: 'p-panelmenu', to: 'mk-nav-list', manual: true, note: '[model] → <mk-nav-group> / <mk-nav-item> children.', docs: '/components/command-nav' },
280
+ { kind: 'element', from: 'p-panelMenu', to: 'mk-nav-list', manual: true, note: '[model] → <mk-nav-group> / <mk-nav-item> children.', docs: '/components/command-nav' },
281
+ { kind: 'element', from: 'p-breadcrumb', to: 'mk-breadcrumb', manual: true, note: '[model] → <mk-breadcrumb-item> children.', docs: '/components/navigation' },
282
+ { kind: 'element', from: 'p-dock', note: 'No dock.', docs: '/components/navigation' },
283
+ { kind: 'element', from: 'p-speeddial', to: 'mk-fab', manual: true, note: '[model] → <button mkFabAction> children.', docs: '/components/navigation' },
284
+ { kind: 'element', from: 'p-speedDial', to: 'mk-fab', manual: true, note: '[model] → <button mkFabAction> children.', docs: '/components/navigation' },
285
+ { kind: 'element', from: 'p-terminal', note: 'No terminal.', docs: '/components/data' },
286
+ ];
287
+ exports.DOCS_BASE = D;
288
+ /** package.json dependencies the migration makes obsolete. */
289
+ exports.PRIMENG_PACKAGES = ['primeng', '@primeng/themes', '@primeuix/themes', '@primeuix/styled', 'primeicons', 'primeflex'];
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "MkKitMigratePrimeng",
4
+ "title": "@mk-kit/ui migrate-primeng schematic",
5
+ "type": "object",
6
+ "properties": {
7
+ "path": {
8
+ "type": "string",
9
+ "default": "src",
10
+ "description": "Directory to migrate (TypeScript and HTML files are visited recursively)."
11
+ },
12
+ "dryRun": {
13
+ "type": "boolean",
14
+ "default": false,
15
+ "description": "Compute and print the report without writing any file.",
16
+ "alias": "d"
17
+ },
18
+ "report": {
19
+ "type": "string",
20
+ "default": "primeng-migration.md",
21
+ "description": "Path of the Markdown report to write (relative to the workspace root)."
22
+ }
23
+ },
24
+ "required": []
25
+ }
@@ -0,0 +1,325 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MK_STYLE_PATH = void 0;
4
+ exports.transformTypeScript = transformTypeScript;
5
+ exports.transformTemplate = transformTemplate;
6
+ exports.transformAngularJson = transformAngularJson;
7
+ exports.transformPackageJson = transformPackageJson;
8
+ exports.renderReport = renderReport;
9
+ /**
10
+ * Pure string transforms behind the `migrate-primeng` schematic. No devkit
11
+ * types here so they can be unit-tested on fixtures and reused by other
12
+ * tooling. Every function returns the new text plus what it did.
13
+ */
14
+ const mapping_1 = require("./mapping");
15
+ const MODULE_BY_PATH = new Map(mapping_1.MODULES.map((m) => [m.path, m]));
16
+ function escapeRe(s) {
17
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18
+ }
19
+ function docsUrl(route) {
20
+ return route ? `${mapping_1.DOCS_BASE}${route}` : undefined;
21
+ }
22
+ /* ------------------------------------------------------------------------ */
23
+ /* TypeScript */
24
+ /* ------------------------------------------------------------------------ */
25
+ const IMPORT_RE = /import\s*(type\s+)?\{([^}]*)\}\s*from\s*['"]primeng\/([\w-]+)['"];?[ \t]*\r?\n?/g;
26
+ /**
27
+ * Rewrites `primeng/*` imports to `@mk-kit/ui`, renames the imported
28
+ * identifiers everywhere in the file, keeps unmapped symbols on their
29
+ * original import, and dedupes `imports: [...]` arrays.
30
+ */
31
+ function transformTypeScript(source) {
32
+ const findings = [];
33
+ const mkNames = new Set();
34
+ const renames = new Map();
35
+ const keep = [];
36
+ let text = source;
37
+ let firstImportIndex = -1;
38
+ text = text.replace(IMPORT_RE, (whole, isType, names, path, offset) => {
39
+ const rule = MODULE_BY_PATH.get(path);
40
+ if (firstImportIndex === -1)
41
+ firstImportIndex = offset;
42
+ const symbols = names
43
+ .split(',')
44
+ .map((n) => n.trim())
45
+ .filter(Boolean);
46
+ const unmapped = [];
47
+ for (const raw of symbols) {
48
+ const [name, alias] = raw.split(/\s+as\s+/).map((s) => s.trim());
49
+ const target = rule?.symbols[name];
50
+ if (target) {
51
+ mkNames.add(target);
52
+ renames.set(alias ?? name, target);
53
+ add(findings, `import:primeng/${path}`, `${name} → ${target}`, 'rewrite', rule?.docs);
54
+ }
55
+ else {
56
+ unmapped.push(raw);
57
+ add(findings, `import:primeng/${path}`, `${name} (primeng/${path}) has no drop-in equivalent — kept`, rule ? 'manual' : 'unmapped', rule?.docs);
58
+ }
59
+ }
60
+ if (unmapped.length)
61
+ keep.push(`import ${isType ?? ''}{ ${unmapped.join(', ')} } from 'primeng/${path}';\n`);
62
+ if (rule?.note && symbols.some((s) => rule.symbols[s.split(/\s+as\s+/)[0].trim()])) {
63
+ add(findings, `note:primeng/${path}`, rule.note, 'manual', rule.docs);
64
+ }
65
+ return '';
66
+ });
67
+ if (firstImportIndex === -1)
68
+ return { text: source, changed: false, findings };
69
+ // Rename identifiers (whole words) — includes `imports: [...]` entries and injected services.
70
+ for (const [from, to] of renames) {
71
+ if (from === to)
72
+ continue;
73
+ text = text.replace(new RegExp(`\\b${escapeRe(from)}\\b`, 'g'), to);
74
+ }
75
+ // Dedupe entries inside `imports: [ ... ]` arrays (several PrimeNG symbols map to one class).
76
+ text = text.replace(/imports:\s*\[([^\]]*)\]/g, (whole, inner) => {
77
+ const seen = new Set();
78
+ const items = inner
79
+ .split(',')
80
+ .map((s) => s.trim())
81
+ .filter((s) => s && !seen.has(s) && seen.add(s));
82
+ const multiline = inner.includes('\n');
83
+ return multiline ? `imports: [\n ${items.join(',\n ')},\n ]` : `imports: [${items.join(', ')}]`;
84
+ });
85
+ // Insert the mk-kit import where the first primeng import was, merging with an existing one.
86
+ const header = [...(keep.length ? keep : [])];
87
+ if (mkNames.size) {
88
+ const existing = /import\s*\{([^}]*)\}\s*from\s*['"]@mk-kit\/ui['"];?[ \t]*\r?\n?/;
89
+ const m = existing.exec(text);
90
+ if (m) {
91
+ const merged = new Set(m[1].split(',').map((s) => s.trim()).filter(Boolean));
92
+ for (const n of mkNames)
93
+ merged.add(n);
94
+ text = text.replace(existing, `import { ${[...merged].join(', ')} } from '@mk-kit/ui';\n`);
95
+ }
96
+ else {
97
+ header.unshift(`import { ${[...mkNames].join(', ')} } from '@mk-kit/ui';\n`);
98
+ }
99
+ }
100
+ text = text.slice(0, firstImportIndex) + header.join('') + text.slice(firstImportIndex);
101
+ // Service usage hints (MessageService / ConfirmationService bodies are not mechanical).
102
+ if (renames.has('MessageService')) {
103
+ text = text.replace(/(\n[ \t]*)([^\n]*\.add\(\s*\{[^\n]*severity[^\n]*)/g, '$1// mk-kit: MessageService.add({severity, summary, detail}) → toast.<severity>(detail, { title: summary })$1$2');
104
+ }
105
+ if (renames.has('ConfirmationService')) {
106
+ text = text.replace(/(\n[ \t]*)([^\n]*\.confirm\(\s*\{[^\n]*)/g, '$1// mk-kit: ConfirmationService.confirm({...}) → await dialog.confirm({ title, message, tone }) returns boolean$1$2');
107
+ }
108
+ // Inline templates get the template pass too.
109
+ const tpl = transformTemplate(text, true);
110
+ text = tpl.text;
111
+ findings.push(...tpl.findings);
112
+ return { text, changed: text !== source, findings };
113
+ }
114
+ /* ------------------------------------------------------------------------ */
115
+ /* Templates */
116
+ /* ------------------------------------------------------------------------ */
117
+ /** Longest selectors first so `p-inputgroupaddon` is not eaten by `p-inputgroup`. */
118
+ const ELEMENT_RULES = mapping_1.SELECTORS.filter((r) => r.kind === 'element').sort((a, b) => b.from.length - a.from.length);
119
+ const ATTRIBUTE_RULES = mapping_1.SELECTORS.filter((r) => r.kind === 'attribute').sort((a, b) => b.from.length - a.from.length);
120
+ function noteComment(rule, inline) {
121
+ const text = `mk-kit: ${rule.from} → ${rule.to ?? 'manual'} — ${rule.note ?? ''}`.trim();
122
+ // Inside a TS template literal `-->` is fine; keep one comment style everywhere.
123
+ return inline ? `<!-- ${text} -->` : `<!-- ${text} -->`;
124
+ }
125
+ function renameAttrs(openTag, attrs) {
126
+ if (!attrs)
127
+ return openTag;
128
+ let out = openTag;
129
+ for (const [from, to] of Object.entries(attrs)) {
130
+ const re = new RegExp(`(\\s)${escapeRe(from)}(?=[\\s=>/])`, 'g');
131
+ out = out.replace(re, (_m, ws) => (to === null ? ws.trimEnd() : `${ws}${to}`));
132
+ }
133
+ return out;
134
+ }
135
+ /**
136
+ * Rewrites PrimeNG selectors in a template (an .html file, or a whole .ts file
137
+ * when `inline` — only `<p-…` tags and known attribute tokens are touched).
138
+ */
139
+ function transformTemplate(source, inline = false) {
140
+ const findings = [];
141
+ let text = source;
142
+ for (const rule of ELEMENT_RULES) {
143
+ const open = new RegExp(`<${escapeRe(rule.from)}(?=[\\s>/])`, 'g');
144
+ const close = new RegExp(`</${escapeRe(rule.from)}\\s*>`, 'g');
145
+ const matches = text.match(open);
146
+ if (!matches)
147
+ continue;
148
+ const n = matches.length;
149
+ if (rule.to) {
150
+ // Rename open/close tags, then attributes on each opening tag.
151
+ text = text.replace(open, `<${rule.to}`).replace(close, `</${rule.to}>`);
152
+ if (rule.attrs) {
153
+ const tagRe = new RegExp(`<${escapeRe(rule.to)}(\\s[^>]*)?>`, 'g');
154
+ text = text.replace(tagRe, (tag) => renameAttrs(tag, rule.attrs));
155
+ }
156
+ add(findings, `element:${rule.from}`, `<${rule.from}> → <${rule.to}>`, 'rewrite', rule.docs, n);
157
+ if (rule.manual && rule.note) {
158
+ text = insertNote(text, `<${rule.to}`, noteComment(rule, inline));
159
+ add(findings, `manual:${rule.from}`, rule.note, 'manual', rule.docs, n);
160
+ }
161
+ }
162
+ else {
163
+ text = insertNote(text, `<${rule.from}`, noteComment(rule, inline));
164
+ add(findings, `element:${rule.from}`, rule.note ?? 'no drop-in equivalent', 'unmapped', rule.docs, n);
165
+ }
166
+ }
167
+ for (const rule of ATTRIBUTE_RULES) {
168
+ // Attribute token forms: `pButton`, `[pTooltip]="…"`, `pTooltip="…"`.
169
+ const token = new RegExp(`(?<=[\\s\\[])${escapeRe(rule.from)}(?=[\\s=\\]>/])`, 'g');
170
+ const matches = text.match(token);
171
+ if (!matches)
172
+ continue;
173
+ const n = matches.length;
174
+ if (rule.to) {
175
+ text = text.replace(token, rule.to);
176
+ if (rule.attrs) {
177
+ const tagRe = new RegExp(`<[a-zA-Z][^>]*\\b${escapeRe(rule.to)}\\b[^>]*>`, 'g');
178
+ text = text.replace(tagRe, (tag) => renameAttrs(tag, rule.attrs));
179
+ }
180
+ add(findings, `attr:${rule.from}`, `${rule.from} → ${rule.to}`, 'rewrite', rule.docs, n);
181
+ if (rule.manual && rule.note) {
182
+ text = insertNoteBeforeTagWith(text, rule.to, noteComment(rule, inline));
183
+ add(findings, `manual:${rule.from}`, rule.note, 'manual', rule.docs, n);
184
+ }
185
+ }
186
+ else {
187
+ text = insertNoteBeforeTagWith(text, rule.from, noteComment(rule, inline));
188
+ add(findings, `attr:${rule.from}`, rule.note ?? 'no drop-in equivalent', 'unmapped', rule.docs, n);
189
+ }
190
+ }
191
+ return { text, changed: text !== source, findings };
192
+ }
193
+ /** Insert `note` once, right before the first occurrence of `needle` (keeps indentation). */
194
+ function insertNote(text, needle, note) {
195
+ if (text.includes(note))
196
+ return text;
197
+ const i = text.indexOf(needle);
198
+ if (i === -1)
199
+ return text;
200
+ const lineStart = text.lastIndexOf('\n', i) + 1;
201
+ const indent = text.slice(lineStart, i).match(/^[ \t]*$/) ? text.slice(lineStart, i) : '';
202
+ return indent ? text.slice(0, lineStart) + indent + note + '\n' + text.slice(lineStart) : text.slice(0, i) + note + ' ' + text.slice(i);
203
+ }
204
+ /** Insert `note` once before the first tag that carries `attr`. */
205
+ function insertNoteBeforeTagWith(text, attr, note) {
206
+ if (text.includes(note))
207
+ return text;
208
+ const re = new RegExp(`<[a-zA-Z][^>]*(?<=[\\s\\[])${escapeRe(attr)}(?=[\\s=\\]>/])`);
209
+ const m = re.exec(text);
210
+ if (!m)
211
+ return text;
212
+ return insertNote(text, m[0], note);
213
+ }
214
+ /* ------------------------------------------------------------------------ */
215
+ /* Workspace files */
216
+ /* ------------------------------------------------------------------------ */
217
+ exports.MK_STYLE_PATH = 'node_modules/@mk-kit/ui/styles/mk-kit.css';
218
+ /** Remove PrimeNG / primeicons / primeflex style entries from every project's `styles`; add the mk-kit theme. */
219
+ function transformAngularJson(source) {
220
+ const findings = [];
221
+ let json;
222
+ try {
223
+ json = JSON.parse(source);
224
+ }
225
+ catch {
226
+ return { text: source, changed: false, findings };
227
+ }
228
+ let changed = false;
229
+ for (const project of Object.values(json.projects ?? {})) {
230
+ const options = project?.architect?.build?.options;
231
+ if (!options || !Array.isArray(options.styles))
232
+ continue;
233
+ const before = options.styles.length;
234
+ options.styles = options.styles.filter((s) => {
235
+ const p = typeof s === 'string' ? s : s?.input ?? '';
236
+ return !/prime(ng|icons|flex|uix)/i.test(p);
237
+ });
238
+ if (options.styles.length !== before) {
239
+ changed = true;
240
+ add(findings, 'styles:primeng', `removed ${before - options.styles.length} PrimeNG/primeicons style entries`, 'rewrite', '/theming', before - options.styles.length);
241
+ }
242
+ const has = options.styles.some((s) => (typeof s === 'string' ? s : s?.input ?? '').includes('@mk-kit/ui/styles'));
243
+ if (!has) {
244
+ options.styles.unshift(exports.MK_STYLE_PATH);
245
+ changed = true;
246
+ add(findings, 'styles:mk-kit', `added ${exports.MK_STYLE_PATH}`, 'rewrite', '/theming');
247
+ }
248
+ }
249
+ return { text: changed ? JSON.stringify(json, null, 2) + '\n' : source, changed, findings };
250
+ }
251
+ /** Drop PrimeNG packages from dependencies and make sure @mk-kit/ui is present. */
252
+ function transformPackageJson(source, packages, mkVersion = '^0.38.0') {
253
+ const findings = [];
254
+ let json;
255
+ try {
256
+ json = JSON.parse(source);
257
+ }
258
+ catch {
259
+ return { text: source, changed: false, findings };
260
+ }
261
+ let changed = false;
262
+ for (const field of ['dependencies', 'devDependencies']) {
263
+ const deps = json[field];
264
+ if (!deps)
265
+ continue;
266
+ for (const p of packages) {
267
+ if (p in deps) {
268
+ delete deps[p];
269
+ changed = true;
270
+ add(findings, 'package:remove', `removed ${p} from ${field}`, 'rewrite');
271
+ }
272
+ }
273
+ }
274
+ json.dependencies ??= {};
275
+ if (!json.dependencies['@mk-kit/ui']) {
276
+ json.dependencies['@mk-kit/ui'] = mkVersion;
277
+ changed = true;
278
+ add(findings, 'package:add', `added @mk-kit/ui ${mkVersion}`, 'rewrite');
279
+ }
280
+ return { text: changed ? JSON.stringify(json, null, 2) + '\n' : source, changed, findings };
281
+ }
282
+ function add(findings, rule, message, kind, docs, count = 1) {
283
+ const existing = findings.find((f) => f.rule === rule && f.message === message);
284
+ if (existing)
285
+ existing.count += count;
286
+ else
287
+ findings.push({ rule, message, kind, docs: docsUrl(docs), count });
288
+ }
289
+ /** Markdown report of a whole run. */
290
+ function renderReport(files, options) {
291
+ const all = files.flatMap((f) => f.findings.map((x) => ({ ...x, file: f.path })));
292
+ const changed = files.filter((f) => f.changed);
293
+ const byKind = (kind) => all.filter((f) => f.kind === kind);
294
+ const sum = (list) => list.reduce((n, f) => n + f.count, 0);
295
+ const lines = [];
296
+ lines.push('# PrimeNG → mk-kit migration report', '');
297
+ lines.push(`${options.dryRun ? '**Dry run** — nothing was written. ' : ''}Scanned ${options.scanned} files, ${options.dryRun ? 'would change' : 'changed'} ${changed.length}.`, '');
298
+ lines.push(`- Automatic rewrites: **${sum(byKind('rewrite'))}**`);
299
+ lines.push(`- Needs a manual touch: **${sum(byKind('manual'))}**`);
300
+ lines.push(`- No mk-kit equivalent: **${sum(byKind('unmapped'))}**`, '');
301
+ lines.push('## Rewrites', '');
302
+ const rewrites = group(byKind('rewrite'));
303
+ lines.push(...(rewrites.length ? rewrites.map(([msg, n]) => `- ${msg} ×${n}`) : ['- (none)']), '');
304
+ lines.push('## Manual steps', '', 'Look for `<!-- mk-kit: … -->` and `// mk-kit:` comments in the changed files.', '');
305
+ const manual = group(byKind('manual'), true);
306
+ lines.push(...(manual.length ? manual.map(([msg, n, docs]) => `- ${msg} ×${n}${docs ? ` — [docs](${docs})` : ''}`) : ['- (none)']), '');
307
+ lines.push('## Not available in mk-kit', '');
308
+ const unmapped = group(byKind('unmapped'), true);
309
+ lines.push(...(unmapped.length ? unmapped.map(([msg, n, docs]) => `- ${msg} ×${n}${docs ? ` — closest: [docs](${docs})` : ''}`) : ['- (none)']), '');
310
+ lines.push('## Files', '');
311
+ lines.push(...(changed.length ? changed.map((f) => `- \`${f.path}\` — ${f.findings.map((x) => `${x.rule}×${x.count}`).join(', ')}`) : ['- (none)']), '');
312
+ lines.push('## Next', '');
313
+ lines.push('1. Run `ng build` and fix the remaining template errors — most are the `<!-- mk-kit -->` notes above.', '2. Replace `MessageService.add(...)` calls with `MkToastService` and `ConfirmationService.confirm(...)` with `await MkDialogService.confirm(...)`.', '3. Icons: `pi pi-*` classes → `<mk-icon name="…" />` (see https://mk-kit.dev/components/icon).', `4. Theme: \`--mk-*\` tokens replace the PrimeNG preset — https://mk-kit.dev/theming.`, '');
314
+ return lines.join('\n');
315
+ }
316
+ function group(list, withDocs = false) {
317
+ const map = new Map();
318
+ for (const f of list) {
319
+ const key = f.message;
320
+ const cur = map.get(key) ?? [0, f.docs];
321
+ cur[0] += f.count;
322
+ map.set(key, cur);
323
+ }
324
+ return [...map.entries()].map(([msg, [n, docs]]) => [msg, n, withDocs ? docs : undefined]);
325
+ }