@dialpad/dialtone-css 8.80.0-next.2 → 8.80.0-next.4

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 (187) hide show
  1. package/lib/build/js/dialtone_migrate_link_rendering/button-nav-test-examples.vue +92 -0
  2. package/lib/build/js/dialtone_migrate_link_rendering/button-nav.test.mjs +272 -0
  3. package/lib/build/js/dialtone_migrate_link_rendering/helpers.mjs +25 -0
  4. package/lib/build/js/dialtone_migrate_link_rendering/index.mjs +1041 -0
  5. package/lib/build/js/dialtone_migrate_link_rendering/link-nav-test-examples.vue +97 -0
  6. package/lib/build/js/dialtone_migrate_link_rendering/link-nav.test.mjs +194 -0
  7. package/lib/build/js/dialtone_migrate_link_rendering/underline-test-examples.vue +57 -0
  8. package/lib/build/js/dialtone_migrate_link_rendering/underline.test.mjs +161 -0
  9. package/lib/build/js/dialtone_migrate_props/index.mjs +794 -0
  10. package/lib/build/js/dialtone_migrate_props/test.mjs +959 -0
  11. package/lib/build/js/dialtone_migration_helper/configs/base-to-semantic.mjs +8 -8
  12. package/lib/build/js/dialtone_migration_helper/configs/size-to-layout.mjs +10 -0
  13. package/lib/build/js/dialtone_migration_helper/configs/success-to-positive.mjs +73 -0
  14. package/lib/build/js/dialtone_migration_helper/configs/utility-class-to-token-stops.mjs +92 -12
  15. package/lib/build/js/dialtone_migration_helper/tests/base-to-semantic-test-examples.vue +10 -10
  16. package/lib/build/js/dialtone_migration_helper/tests/base-to-semantic.test.mjs +8 -8
  17. package/lib/build/js/dialtone_migration_helper/tests/size-to-layout-test-examples.vue +16 -0
  18. package/lib/build/js/dialtone_migration_helper/tests/size-to-layout.test.mjs +87 -0
  19. package/lib/build/js/dialtone_migration_helper/tests/success-to-positive-test-examples.vue +166 -0
  20. package/lib/build/js/dialtone_migration_helper/tests/success-to-positive.test.mjs +287 -0
  21. package/lib/build/js/dialtone_migration_helper/tests/utility-class-to-token-stops-radius-examples.vue +66 -0
  22. package/lib/build/js/dialtone_migration_helper/tests/utility-class-to-token-stops.test.mjs +170 -0
  23. package/lib/build/less/components/badge.less +1 -1
  24. package/lib/build/less/components/banner.less +1 -1
  25. package/lib/build/less/components/box.less +9 -22
  26. package/lib/build/less/components/description-list.less +4 -0
  27. package/lib/build/less/components/forms.less +4 -2
  28. package/lib/build/less/components/input.less +2 -2
  29. package/lib/build/less/components/link.less +18 -4
  30. package/lib/build/less/components/modal.less +1 -1
  31. package/lib/build/less/components/notice.less +3 -3
  32. package/lib/build/less/components/progress_circle.less +10 -2
  33. package/lib/build/less/components/prose.less +512 -0
  34. package/lib/build/less/components/rich-text-editor.less +31 -0
  35. package/lib/build/less/components/selects.less +1 -1
  36. package/lib/build/less/components/text.less +2 -0
  37. package/lib/build/less/components/toast.less +1 -1
  38. package/lib/build/less/dialtone.less +1 -0
  39. package/lib/build/less/recipes/leftbar_row.less +2 -2
  40. package/lib/build/less/recipes/settings_menu_button.less +1 -1
  41. package/lib/build/less/recipes/top_banner_info.less +1 -1
  42. package/lib/build/less/utilities/backgrounds.less +12 -0
  43. package/lib/build/less/utilities/borders.less +56 -89
  44. package/lib/build/less/utilities/colors.less +8 -0
  45. package/lib/build/less/utilities/effects.less +1 -0
  46. package/lib/build/less/utilities/flex.less +145 -18
  47. package/lib/build/less/utilities/grid.less +40 -152
  48. package/lib/build/less/utilities/layout.less +19 -7
  49. package/lib/build/less/utilities/sizing.less +148 -143
  50. package/lib/build/less/variables/visual-styles.less +2 -1
  51. package/lib/dist/dialtone-default-theme.css +2840 -1807
  52. package/lib/dist/dialtone-default-theme.min.css +1 -1
  53. package/lib/dist/dialtone-docs.json +1 -1
  54. package/lib/dist/dialtone.css +2747 -1756
  55. package/lib/dist/dialtone.min.css +1 -1
  56. package/lib/dist/js/dialtone_migrate_link_rendering/button-nav-test-examples.vue +92 -0
  57. package/lib/dist/js/dialtone_migrate_link_rendering/button-nav.test.mjs +272 -0
  58. package/lib/dist/js/dialtone_migrate_link_rendering/helpers.mjs +25 -0
  59. package/lib/dist/js/dialtone_migrate_link_rendering/index.mjs +1041 -0
  60. package/lib/dist/js/dialtone_migrate_link_rendering/link-nav-test-examples.vue +97 -0
  61. package/lib/dist/js/dialtone_migrate_link_rendering/link-nav.test.mjs +194 -0
  62. package/lib/dist/js/dialtone_migrate_link_rendering/underline-test-examples.vue +57 -0
  63. package/lib/dist/js/dialtone_migrate_link_rendering/underline.test.mjs +161 -0
  64. package/lib/dist/js/dialtone_migrate_props/index.mjs +794 -0
  65. package/lib/dist/js/dialtone_migrate_props/test.mjs +959 -0
  66. package/lib/dist/js/dialtone_migration_helper/configs/base-to-semantic.mjs +8 -8
  67. package/lib/dist/js/dialtone_migration_helper/configs/size-to-layout.mjs +10 -0
  68. package/lib/dist/js/dialtone_migration_helper/configs/success-to-positive.mjs +73 -0
  69. package/lib/dist/js/dialtone_migration_helper/configs/utility-class-to-token-stops.mjs +92 -12
  70. package/lib/dist/js/dialtone_migration_helper/tests/base-to-semantic-test-examples.vue +10 -10
  71. package/lib/dist/js/dialtone_migration_helper/tests/base-to-semantic.test.mjs +8 -8
  72. package/lib/dist/js/dialtone_migration_helper/tests/size-to-layout-test-examples.vue +16 -0
  73. package/lib/dist/js/dialtone_migration_helper/tests/size-to-layout.test.mjs +87 -0
  74. package/lib/dist/js/dialtone_migration_helper/tests/success-to-positive-test-examples.vue +166 -0
  75. package/lib/dist/js/dialtone_migration_helper/tests/success-to-positive.test.mjs +287 -0
  76. package/lib/dist/js/dialtone_migration_helper/tests/utility-class-to-token-stops-radius-examples.vue +66 -0
  77. package/lib/dist/js/dialtone_migration_helper/tests/utility-class-to-token-stops.test.mjs +170 -0
  78. package/lib/dist/tokens/tokens-101-dark.css +81 -45
  79. package/lib/dist/tokens/tokens-101-light.css +75 -39
  80. package/lib/dist/tokens/tokens-102-dark.css +81 -45
  81. package/lib/dist/tokens/tokens-102-light.css +75 -39
  82. package/lib/dist/tokens/tokens-103-dark.css +81 -45
  83. package/lib/dist/tokens/tokens-103-light.css +75 -39
  84. package/lib/dist/tokens/tokens-104-dark.css +81 -45
  85. package/lib/dist/tokens/tokens-104-light.css +75 -39
  86. package/lib/dist/tokens/tokens-105-dark.css +81 -45
  87. package/lib/dist/tokens/tokens-105-light.css +75 -39
  88. package/lib/dist/tokens/tokens-106-dark.css +81 -45
  89. package/lib/dist/tokens/tokens-106-light.css +75 -39
  90. package/lib/dist/tokens/tokens-107-dark.css +81 -45
  91. package/lib/dist/tokens/tokens-107-light.css +75 -39
  92. package/lib/dist/tokens/tokens-108-dark.css +81 -45
  93. package/lib/dist/tokens/tokens-108-light.css +75 -39
  94. package/lib/dist/tokens/tokens-109-dark.css +81 -45
  95. package/lib/dist/tokens/tokens-109-light.css +75 -39
  96. package/lib/dist/tokens/tokens-110-dark.css +81 -45
  97. package/lib/dist/tokens/tokens-110-light.css +75 -39
  98. package/lib/dist/tokens/tokens-111-dark.css +81 -45
  99. package/lib/dist/tokens/tokens-111-light.css +75 -39
  100. package/lib/dist/tokens/tokens-112-dark.css +81 -45
  101. package/lib/dist/tokens/tokens-112-light.css +75 -39
  102. package/lib/dist/tokens/tokens-113-dark.css +81 -45
  103. package/lib/dist/tokens/tokens-113-light.css +75 -39
  104. package/lib/dist/tokens/tokens-114-dark.css +81 -45
  105. package/lib/dist/tokens/tokens-114-light.css +75 -39
  106. package/lib/dist/tokens/tokens-115-dark.css +81 -45
  107. package/lib/dist/tokens/tokens-115-light.css +75 -39
  108. package/lib/dist/tokens/tokens-116-dark.css +81 -45
  109. package/lib/dist/tokens/tokens-116-light.css +75 -39
  110. package/lib/dist/tokens/tokens-117-dark.css +81 -45
  111. package/lib/dist/tokens/tokens-117-light.css +75 -39
  112. package/lib/dist/tokens/tokens-118-dark.css +81 -45
  113. package/lib/dist/tokens/tokens-118-light.css +75 -39
  114. package/lib/dist/tokens/tokens-119-dark.css +81 -45
  115. package/lib/dist/tokens/tokens-119-light.css +75 -39
  116. package/lib/dist/tokens/tokens-120-dark.css +81 -45
  117. package/lib/dist/tokens/tokens-120-light.css +75 -39
  118. package/lib/dist/tokens/tokens-121-dark.css +81 -45
  119. package/lib/dist/tokens/tokens-121-light.css +75 -39
  120. package/lib/dist/tokens/tokens-122-dark.css +81 -45
  121. package/lib/dist/tokens/tokens-122-light.css +75 -39
  122. package/lib/dist/tokens/tokens-123-dark.css +81 -45
  123. package/lib/dist/tokens/tokens-123-light.css +75 -39
  124. package/lib/dist/tokens/tokens-124-dark.css +81 -45
  125. package/lib/dist/tokens/tokens-124-light.css +75 -39
  126. package/lib/dist/tokens/tokens-125-dark.css +81 -45
  127. package/lib/dist/tokens/tokens-125-light.css +75 -39
  128. package/lib/dist/tokens/tokens-126-dark.css +81 -45
  129. package/lib/dist/tokens/tokens-126-light.css +75 -39
  130. package/lib/dist/tokens/tokens-127-dark.css +81 -45
  131. package/lib/dist/tokens/tokens-127-light.css +75 -39
  132. package/lib/dist/tokens/tokens-128-dark.css +81 -45
  133. package/lib/dist/tokens/tokens-128-light.css +75 -39
  134. package/lib/dist/tokens/tokens-129-dark.css +81 -45
  135. package/lib/dist/tokens/tokens-129-light.css +75 -39
  136. package/lib/dist/tokens/tokens-130-dark.css +81 -45
  137. package/lib/dist/tokens/tokens-130-light.css +75 -39
  138. package/lib/dist/tokens/tokens-131-dark.css +81 -45
  139. package/lib/dist/tokens/tokens-131-light.css +75 -39
  140. package/lib/dist/tokens/tokens-132-dark.css +81 -45
  141. package/lib/dist/tokens/tokens-132-light.css +75 -39
  142. package/lib/dist/tokens/tokens-133-dark.css +81 -45
  143. package/lib/dist/tokens/tokens-133-light.css +75 -39
  144. package/lib/dist/tokens/tokens-134-dark.css +81 -45
  145. package/lib/dist/tokens/tokens-134-light.css +75 -39
  146. package/lib/dist/tokens/tokens-135-dark.css +81 -45
  147. package/lib/dist/tokens/tokens-135-light.css +75 -39
  148. package/lib/dist/tokens/tokens-136-dark.css +81 -45
  149. package/lib/dist/tokens/tokens-136-light.css +75 -39
  150. package/lib/dist/tokens/tokens-137-dark.css +81 -45
  151. package/lib/dist/tokens/tokens-137-light.css +75 -39
  152. package/lib/dist/tokens/tokens-aegean-dark.css +81 -45
  153. package/lib/dist/tokens/tokens-aegean-light.css +75 -39
  154. package/lib/dist/tokens/tokens-base-dark.css +18 -12
  155. package/lib/dist/tokens/tokens-base-light.css +18 -12
  156. package/lib/dist/tokens/tokens-botany-dark.css +81 -45
  157. package/lib/dist/tokens/tokens-botany-light.css +75 -39
  158. package/lib/dist/tokens/tokens-buttercream-dark.css +81 -45
  159. package/lib/dist/tokens/tokens-buttercream-light.css +75 -39
  160. package/lib/dist/tokens/tokens-ceruleo-dark.css +81 -45
  161. package/lib/dist/tokens/tokens-ceruleo-light.css +75 -39
  162. package/lib/dist/tokens/tokens-debug-base.css +6 -0
  163. package/lib/dist/tokens/tokens-debug-dp.css +39 -3
  164. package/lib/dist/tokens/tokens-dp-dark.css +81 -45
  165. package/lib/dist/tokens/tokens-dp-light.css +75 -39
  166. package/lib/dist/tokens/tokens-expressive-dark.css +81 -45
  167. package/lib/dist/tokens/tokens-expressive-light.css +75 -39
  168. package/lib/dist/tokens/tokens-expressive-sm-dark.css +81 -45
  169. package/lib/dist/tokens/tokens-expressive-sm-light.css +75 -39
  170. package/lib/dist/tokens/tokens-high-desert-dark.css +81 -45
  171. package/lib/dist/tokens/tokens-high-desert-light.css +75 -39
  172. package/lib/dist/tokens/tokens-melon-dark.css +81 -45
  173. package/lib/dist/tokens/tokens-melon-light.css +75 -39
  174. package/lib/dist/tokens/tokens-plum-dark.css +81 -45
  175. package/lib/dist/tokens/tokens-plum-light.css +75 -39
  176. package/lib/dist/tokens/tokens-prota-deuter-dark.css +79 -43
  177. package/lib/dist/tokens/tokens-prota-deuter-light.css +74 -38
  178. package/lib/dist/tokens/tokens-sunflower-dark.css +81 -45
  179. package/lib/dist/tokens/tokens-sunflower-light.css +75 -39
  180. package/lib/dist/tokens/tokens-tmo-dark.css +81 -45
  181. package/lib/dist/tokens/tokens-tmo-light.css +75 -39
  182. package/lib/dist/tokens/tokens-trita-dark.css +81 -45
  183. package/lib/dist/tokens/tokens-trita-light.css +75 -39
  184. package/lib/dist/tokens/tokens-verdant-haze-dark.css +81 -45
  185. package/lib/dist/tokens/tokens-verdant-haze-light.css +75 -39
  186. package/lib/dist/tokens-docs.json +1 -1
  187. package/package.json +10 -5
@@ -0,0 +1,794 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable max-lines */
3
+
4
+ /**
5
+ * @fileoverview Migration script for Dialtone component prop/slot/event breaking changes.
6
+ *
7
+ * Covers these changes:
8
+ * DLT-3161 avatar: clickable → interactive
9
+ * DLT-3157 kind/validation-state values: danger→critical, error→critical, success→positive
10
+ * also: tone="success" → tone="positive" (dt-text, dt-link)
11
+ * also: link-kind="success" → link-kind="positive" (dt-button)
12
+ * DLT-3159 positive boolean props: hide-close→show-close, hide-icon→show-icon, etc.
13
+ * DLT-3282 show prop → open (modal, toast, tooltip) + update:show → update:open
14
+ * DLT-3283 slot renames: titleOverride→header, labelSlot→label, headingSlot→heading
15
+ * DLT-3284 title → headerText on banner, notice, toast, modal; title-id → header-id on banner, notice, toast only
16
+ * DLT-3159 label-visible → show-label (checkbox, combobox, input, select-menu, toggle, etc.)
17
+ * DLT-3160 checkbox-group: selectedValues → modelValue; @input/@change → @update:model-value on form inputs
18
+ * DLT-3100 rootClass removed — warns with file locations, cannot auto-migrate
19
+ *
20
+ * Usage:
21
+ * npx dialtone-migrate-props [options]
22
+ *
23
+ * Options:
24
+ * --cwd <path> Working directory (default: current directory)
25
+ * --dry-run Show changes without applying them
26
+ * --yes Apply all changes without prompting
27
+ * --help Show help
28
+ *
29
+ * Examples:
30
+ * npx dialtone-migrate-props
31
+ * npx dialtone-migrate-props --dry-run
32
+ * npx dialtone-migrate-props --cwd ./src
33
+ */
34
+
35
+ import fs from 'fs/promises';
36
+ import { realpathSync } from 'node:fs';
37
+ import path from 'path';
38
+ import readline from 'readline';
39
+ import { fileURLToPath } from 'node:url';
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Utilities
43
+ // ---------------------------------------------------------------------------
44
+
45
+ function kebabToCamel (str) {
46
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
47
+ }
48
+
49
+ function escapeRe (str) {
50
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Transformation Data
55
+ // ---------------------------------------------------------------------------
56
+
57
+ // Components where `show` → `open` and `update:show` → `update:open`
58
+ const SHOW_TO_OPEN_COMPONENTS = new Set(['dt-modal', 'dt-toast', 'dt-tooltip']);
59
+
60
+ // Direct prop renames per component (kebab-case keys; camelCase handled automatically)
61
+ const COMPONENT_PROP_RENAMES = {
62
+ 'dt-avatar': {
63
+ clickable: 'interactive',
64
+ },
65
+ 'dt-banner': {
66
+ title: 'header-text',
67
+ 'title-id': 'header-id',
68
+ },
69
+ 'dt-notice': {
70
+ title: 'header-text',
71
+ 'title-id': 'header-id',
72
+ },
73
+ 'dt-toast': {
74
+ title: 'header-text',
75
+ 'title-id': 'header-id',
76
+ },
77
+ 'dt-modal': {
78
+ title: 'header-text',
79
+ 'banner-title': 'banner-header-text',
80
+ },
81
+ 'dt-checkbox': { 'label-visible': 'show-label' },
82
+ 'dt-combobox': { 'label-visible': 'show-label' },
83
+ 'dt-combobox-multi-select': { 'label-visible': 'show-label' },
84
+ 'dt-combobox-with-popover': { 'label-visible': 'show-label' },
85
+ 'dt-input': { 'label-visible': 'show-label' },
86
+ 'dt-select-menu': { 'label-visible': 'show-label' },
87
+ 'dt-toggle': { 'label-visible': 'show-label' },
88
+ 'dt-radio-group': { 'label-visible': 'show-label' },
89
+ 'dt-checkbox-group': { 'selected-values': 'model-value' },
90
+ 'dt-link': { kind: 'tone' },
91
+ 'dt-popover': { 'hide-on-click': 'close-on-click' },
92
+ };
93
+
94
+ // Removed class props per component: old-prop → class attribute directly
95
+ const ROOT_CLASS_RENAMES = {
96
+ 'dt-input': 'root-class',
97
+ 'dt-checkbox': 'root-class',
98
+ 'dt-radio': 'root-class',
99
+ 'dt-select-menu': 'root-class',
100
+ 'dt-toggle': 'wrapper-class',
101
+ 'dt-card': 'container-class',
102
+ 'dt-breadcrumb-item': 'root-class',
103
+ 'dt-split-button': 'root-class',
104
+ 'dt-feed-item-pill': 'wrapper-class',
105
+ };
106
+
107
+ // Inverted boolean props per component: `hide-X` → `:show-X="false"`
108
+ const INVERTED_BOOL_PROPS = {
109
+ 'dt-banner': { 'hide-close': 'show-close', 'hide-icon': 'show-icon', 'hide-action': 'show-action' },
110
+ 'dt-chip': { 'hide-close': 'show-close' },
111
+ 'dt-modal': { 'hide-close': 'show-close' },
112
+ 'dt-notice': { 'hide-close': 'show-close', 'hide-icon': 'show-icon', 'hide-action': 'show-action' },
113
+ 'dt-toast': { 'hide-close': 'show-close', 'hide-icon': 'show-icon', 'hide-action': 'show-action' },
114
+ 'dt-filter-pill': { 'hide-clear': 'show-clear' },
115
+ 'dt-notice-action': { 'hide-close': 'show-close', 'hide-action': 'show-action' },
116
+ 'dt-pagination': { 'hide-edges': 'show-edges' },
117
+ 'dt-segmented-control': { 'hide-divider': 'show-divider' },
118
+ 'dt-rich-text-editor': { 'prevent-typing': 'allow-typing', 'hide-link-bubble-menu': 'show-link-bubble-menu' },
119
+ 'dt-recipe-message-input': { 'prevent-typing': 'allow-typing' },
120
+ 'dt-recipe-contact-centers-row': { 'hide-actions': 'show-actions' },
121
+ };
122
+
123
+ // Prop value renames applied to any dt-* component
124
+ const PROP_VALUE_RENAMES = [
125
+ { prop: 'kind', oldValue: 'danger', newValue: 'critical' },
126
+ { prop: 'kind', oldValue: 'error', newValue: 'critical' },
127
+ { prop: 'kind', oldValue: 'success', newValue: 'positive' },
128
+ { prop: 'validation-state', oldValue: 'error', newValue: 'critical' },
129
+ { prop: 'validation-state', oldValue: 'success', newValue: 'positive' },
130
+ { prop: 'type', oldValue: 'success', newValue: 'positive' },
131
+ { prop: 'banner-kind', oldValue: 'error', newValue: 'critical' },
132
+ { prop: 'banner-kind', oldValue: 'success', newValue: 'positive' },
133
+ // dt-text uses `tone` for foreground tokens; dt-link migrates kind → tone in the
134
+ // same pass, so any tag that ends up with tone="success" should land on positive.
135
+ { prop: 'tone', oldValue: 'success', newValue: 'positive' },
136
+ { prop: 'tone', oldValue: 'success-strong', newValue: 'positive-strong' },
137
+ // dt-button has a separate `link-kind` prop for link-styled buttons; its
138
+ // pre-DLT-3157 value `success` aliases to `positive`.
139
+ { prop: 'link-kind', oldValue: 'danger', newValue: 'critical' },
140
+ { prop: 'link-kind', oldValue: 'success', newValue: 'positive' },
141
+ // dt-box uses `surface` and `bc` for its surface and border-color modifiers.
142
+ // The validator only declares positive*; legacy success* values trigger a warning.
143
+ { prop: 'surface', oldValue: 'success', newValue: 'positive' },
144
+ { prop: 'surface', oldValue: 'success-subtle', newValue: 'positive-subtle' },
145
+ { prop: 'surface', oldValue: 'success-strong', newValue: 'positive-strong' },
146
+ { prop: 'surface', oldValue: 'success-opaque', newValue: 'positive-opaque' },
147
+ { prop: 'surface', oldValue: 'success-subtle-opaque', newValue: 'positive-subtle-opaque' },
148
+ { prop: 'bc', oldValue: 'success', newValue: 'positive' },
149
+ { prop: 'bc', oldValue: 'success-subtle', newValue: 'positive-subtle' },
150
+ { prop: 'bc', oldValue: 'success-strong', newValue: 'positive-strong' },
151
+ ];
152
+
153
+ // Slot renames — applied globally (names are unique enough across the system)
154
+ const SLOT_RENAMES = [
155
+ { old: 'titleOverride', new: 'header' },
156
+ { old: 'labelSlot', new: 'label' },
157
+ { old: 'headingSlot', new: 'heading' },
158
+ ];
159
+
160
+ // v-model arg renames per component (null = drop arg, becomes default v-model)
161
+ const VMODEL_ARG_RENAMES = {
162
+ 'dt-checkbox-group': { 'selected-values': null, selectedValues: null },
163
+ };
164
+
165
+ // @update:* event renames per component
166
+ const UPDATE_EVENT_RENAMES = {
167
+ 'dt-checkbox-group': { 'selected-values': 'model-value', selectedValues: 'modelValue' },
168
+ };
169
+
170
+ // Component events removed from emits and replaced by @update:model-value
171
+ const EMIT_EVENT_RENAMES = {
172
+ 'dt-input': { input: 'update:model-value' },
173
+ 'dt-radio': { input: 'update:model-value' },
174
+ 'dt-radio-group': { input: 'update:model-value' },
175
+ 'dt-select-menu': { input: 'update:model-value', change: 'update:model-value' },
176
+ 'dt-toggle': { change: 'update:model-value' },
177
+ 'dt-combobox-multi-select': { input: 'update:model-value' },
178
+ 'dt-rich-text-editor': { input: 'update:model-value' },
179
+ 'dt-input-group': { input: 'update:model-value' },
180
+ };
181
+
182
+ // Regex matching any Dialtone component opening tag, handling > inside quoted attribute values.
183
+ // Alternation `(?:"[^"]*"|'[^']*')` consumes quoted strings before the bare [^>]* can stop at >.
184
+ const DT_TAG_RE = /<(dt-[\w-]+|Dt\w+)\b((?:[^>"']*(?:"[^"]*"|'[^']*'))*[^>]*)(?:\/?>)/g;
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Per-tag Sub-Transformers
188
+ // ---------------------------------------------------------------------------
189
+
190
+ /**
191
+ * `show` → `open` plus `@update:show` / `v-model:show` on overlay components.
192
+ */
193
+ function applyShowToOpen (tag, canonical) {
194
+ if (!SHOW_TO_OPEN_COMPONENTS.has(canonical)) return { tag, count: 0 };
195
+ let result = tag;
196
+ let count = 0;
197
+
198
+ // :show="…" or show="…" but NOT show-close / show-icon / show-action etc.
199
+ result = result.replace(/(\b:?)(show)(?=[=\s/>])/g, (match, colon, _prop, offset, str) => {
200
+ if (str[offset + match.length] === '-') return match;
201
+ count++;
202
+ return `${colon}open`;
203
+ });
204
+
205
+ const updateBefore = result;
206
+ result = result.replace(/@update:show\b/g, '@update:open');
207
+ count += (result !== updateBefore) ? 1 : 0;
208
+
209
+ const vmodelBefore = result;
210
+ result = result.replace(/v-model:show\b/g, 'v-model:open');
211
+ count += (result !== vmodelBefore) ? 1 : 0;
212
+
213
+ return { tag: result, count };
214
+ }
215
+
216
+ /**
217
+ * Direct 1:1 prop renames for a specific component.
218
+ */
219
+ function applyPropRenames (tag, canonical) {
220
+ const renames = COMPONENT_PROP_RENAMES[canonical];
221
+ if (!renames) return { tag, count: 0 };
222
+
223
+ let result = tag;
224
+ let count = 0;
225
+
226
+ for (const [oldProp, newProp] of Object.entries(renames)) {
227
+ const oldCamel = kebabToCamel(oldProp);
228
+ const newCamel = kebabToCamel(newProp);
229
+
230
+ // Static kebab: `old-prop="…"` or bare boolean
231
+ const before1 = result;
232
+ result = result.replace(new RegExp(`(?<!:)\\b${escapeRe(oldProp)}(?=[=\\s/>])`, 'g'), newProp);
233
+ if (result !== before1) { count++; continue; }
234
+
235
+ // Bound kebab: `:old-prop="…"`
236
+ const before2 = result;
237
+ result = result.replace(new RegExp(`(?<=:)${escapeRe(oldProp)}(?==)`, 'g'), newProp);
238
+ if (result !== before2) { count++; continue; }
239
+
240
+ // Bound camelCase: `:oldProp="…"` / Static camelCase: `oldProp="…"` or bare boolean
241
+ if (oldCamel !== oldProp) {
242
+ const before3 = result;
243
+ result = result.replace(new RegExp(`(?<=:)${escapeRe(oldCamel)}(?==)`, 'g'), newCamel);
244
+ if (result !== before3) { count++; continue; }
245
+ const before4 = result;
246
+ result = result.replace(new RegExp(`(?<!:)\\b${escapeRe(oldCamel)}(?=[=\\s/>])`, 'g'), newCamel);
247
+ if (result !== before4) count++;
248
+ }
249
+ }
250
+
251
+ return { tag: result, count };
252
+ }
253
+
254
+ /**
255
+ * Apply one inverted-boolean rename: hide-X → :show-X="false".
256
+ * Returns { tag, count, warning? }
257
+ */
258
+ function applyOneInvertedProp (tag, oldProp, newProp) {
259
+ const oldCamel = kebabToCamel(oldProp);
260
+ const newCamel = kebabToCamel(newProp);
261
+
262
+ // `:hide-prop="false"` or `:hideXxx="false"` → remove (default is true)
263
+ for (const p of [escapeRe(oldProp), escapeRe(oldCamel)]) {
264
+ const before = tag;
265
+ tag = tag.replace(new RegExp(`:${p}="false"`, 'g'), '');
266
+ if (tag !== before) return { tag, count: 1 };
267
+ }
268
+
269
+ // `:hide-prop="true"` → `:show-prop="false"`
270
+ const trueBefore = tag;
271
+ tag = tag.replace(new RegExp(`:${escapeRe(oldProp)}="true"`, 'g'), `:${newProp}="false"`);
272
+ if (tag !== trueBefore) return { tag, count: 1 };
273
+
274
+ // `:hideXxx="true"` → `:showXxx="false"`
275
+ const camelTrueBefore = tag;
276
+ tag = tag.replace(new RegExp(`:${escapeRe(oldCamel)}="true"`, 'g'), `:${newCamel}="false"`);
277
+ if (tag !== camelTrueBefore) return { tag, count: 1 };
278
+
279
+ // Bare boolean: `hide-prop` → `:show-prop="false"`
280
+ const bareBefore = tag;
281
+ tag = tag.replace(new RegExp(`(?<!:)\\b${escapeRe(oldProp)}(?=[\\s/>])`, 'g'), `:${newProp}="false"`);
282
+ if (tag !== bareBefore) return { tag, count: 1 };
283
+
284
+ // Bound expression: cannot auto-invert — emit warning (check both kebab and camelCase forms)
285
+ for (const [attrProp, attrNew] of [[oldProp, newProp], [oldCamel, newCamel]]) {
286
+ const exprMatch = new RegExp(`:${escapeRe(attrProp)}="([^"]*)"`, 'g').exec(tag);
287
+ if (exprMatch) {
288
+ return {
289
+ tag,
290
+ count: 0,
291
+ warning: `Cannot auto-invert :${attrProp}="${exprMatch[1]}" → :${attrNew}. ` +
292
+ `Replace manually with :${attrNew}="!(${exprMatch[1]})"`,
293
+ };
294
+ }
295
+ }
296
+
297
+ return { tag, count: 0 };
298
+ }
299
+
300
+ /**
301
+ * Inverted boolean props for a specific component (hide-X → :show-X="false").
302
+ */
303
+ function applyInvertedBoolProps (tag, canonical) {
304
+ const invertedMap = INVERTED_BOOL_PROPS[canonical];
305
+ if (!invertedMap) return { tag, count: 0, warnings: [] };
306
+
307
+ let result = tag;
308
+ let count = 0;
309
+ const warnings = [];
310
+
311
+ for (const [oldProp, newProp] of Object.entries(invertedMap)) {
312
+ const { tag: updated, count: c, warning } = applyOneInvertedProp(result, oldProp, newProp);
313
+ result = updated;
314
+ count += c;
315
+ if (warning) warnings.push(warning);
316
+ }
317
+
318
+ return { tag: result, count, warnings };
319
+ }
320
+
321
+ /**
322
+ * `kind` and `validation-state` value renames on any dt-* component.
323
+ */
324
+ function applyPropValueRenames (tag) {
325
+ let result = tag;
326
+ let count = 0;
327
+
328
+ for (const { prop, oldValue, newValue } of PROP_VALUE_RENAMES) {
329
+ const propCamel = kebabToCamel(prop);
330
+
331
+ for (const p of [prop, propCamel]) {
332
+ // Static: `prop="oldValue"`
333
+ const before1 = result;
334
+ result = result.replace(
335
+ new RegExp(`(?<!:)\\b${escapeRe(p)}="${escapeRe(oldValue)}"`, 'g'),
336
+ `${p}="${newValue}"`,
337
+ );
338
+ if (result !== before1) count++;
339
+
340
+ // Bound string literal: `:prop="'oldValue'"`
341
+ const before2 = result;
342
+ result = result.replace(
343
+ new RegExp(`:${escapeRe(p)}="'${escapeRe(oldValue)}'"`, 'g'),
344
+ `:${p}="'${newValue}'"`,
345
+ );
346
+ if (result !== before2) count++;
347
+ }
348
+ }
349
+
350
+ return { tag: result, count };
351
+ }
352
+
353
+ /**
354
+ * Renames a removed class prop (root-class, wrapper-class, container-class) to `class`.
355
+ * Static values are merged into an existing `class="…"` if present.
356
+ * Dynamic bindings warn if `:class` already exists (cannot safely merge expressions).
357
+ */
358
+ function applyRootClassRename (tag, canonical) {
359
+ const oldProp = ROOT_CLASS_RENAMES[canonical];
360
+ if (!oldProp) return { tag, count: 0, warnings: [] };
361
+
362
+ const oldCamel = kebabToCamel(oldProp);
363
+ let result = tag;
364
+ let count = 0;
365
+ const warnings = [];
366
+
367
+ for (const p of [oldProp, oldCamel]) {
368
+ // Dynamic: `:old-prop="expr"` → `:class="expr"` (warn if :class already exists)
369
+ const dynRe = new RegExp(`:${escapeRe(p)}="([^"]*)"`);
370
+ const dynMatch = dynRe.exec(result);
371
+ if (dynMatch) {
372
+ if (/:class="/.test(result)) {
373
+ warnings.push(
374
+ `Cannot auto-merge :${p}="${dynMatch[1]}" into :class on <${canonical}> — merge manually.`,
375
+ );
376
+ } else {
377
+ result = result.replace(dynMatch[0], `:class="${dynMatch[1]}"`);
378
+ count++;
379
+ }
380
+ break;
381
+ }
382
+
383
+ // Static: `old-prop="value"` → `class="value"` (merge if class already exists)
384
+ const staticRe = new RegExp(`(?<!:)\\b${escapeRe(p)}="([^"]*)"`);
385
+ const staticMatch = staticRe.exec(result);
386
+ if (staticMatch) {
387
+ const addedVal = staticMatch[1];
388
+ const existingClassMatch = /(?<![:\w-])class="([^"]*)"/.exec(result);
389
+ if (existingClassMatch) {
390
+ result = result.replace(staticMatch[0], '');
391
+ result = result.replace(existingClassMatch[0], `class="${existingClassMatch[1]} ${addedVal}"`);
392
+ } else {
393
+ result = result.replace(staticMatch[0], `class="${addedVal}"`);
394
+ }
395
+ count++;
396
+ break;
397
+ }
398
+ }
399
+
400
+ return { tag: result, count, warnings };
401
+ }
402
+
403
+ function applyVModelArgRenames (tag, canonical) {
404
+ const vmodelMap = VMODEL_ARG_RENAMES[canonical];
405
+ if (!vmodelMap) return { tag, count: 0 };
406
+ let result = tag;
407
+ let count = 0;
408
+ for (const [oldArg, newArg] of Object.entries(vmodelMap)) {
409
+ const before = result;
410
+ const replacement = newArg ? `v-model:${newArg}` : 'v-model';
411
+ result = result.replace(new RegExp(`v-model:${escapeRe(oldArg)}\\b`, 'g'), replacement);
412
+ if (result !== before) count++;
413
+ }
414
+ return { tag: result, count };
415
+ }
416
+
417
+ function applyUpdateEventRenames (tag, canonical) {
418
+ const eventMap = UPDATE_EVENT_RENAMES[canonical];
419
+ if (!eventMap) return { tag, count: 0 };
420
+ let result = tag;
421
+ let count = 0;
422
+ for (const [oldEvt, newEvt] of Object.entries(eventMap)) {
423
+ const before = result;
424
+ result = result.replace(new RegExp(`@update:${escapeRe(oldEvt)}\\b`, 'g'), `@update:${newEvt}`);
425
+ if (result !== before) count++;
426
+ }
427
+ return { tag: result, count };
428
+ }
429
+
430
+ /**
431
+ * `v-model:oldArg` → `v-model` and `@update:oldEvent` → `@update:newEvent` per component.
432
+ */
433
+ function applyVModelAndEventRenames (tag, canonical) {
434
+ const r1 = applyVModelArgRenames(tag, canonical);
435
+ const r2 = applyUpdateEventRenames(r1.tag, canonical);
436
+ return { tag: r2.tag, count: r1.count + r2.count };
437
+ }
438
+
439
+ /**
440
+ * Remove duplicate `@eventName` bindings within a single tag string, keeping
441
+ * the first occurrence. This handles the case where multiple source events
442
+ * (e.g. `@input` and `@change`) both map to the same target event.
443
+ */
444
+ function deduplicateEventBindings (tag) {
445
+ const seen = new Set();
446
+ let removedCount = 0;
447
+ const cleaned = tag.replace(/ @([\w:.-]+)(?:=(?:"[^"]*"|'[^']*'))?/g, (match, eventName) => {
448
+ if (seen.has(eventName)) {
449
+ removedCount++;
450
+ return '';
451
+ }
452
+ seen.add(eventName);
453
+ return match;
454
+ });
455
+ return { tag: cleaned, removedCount };
456
+ }
457
+
458
+ /**
459
+ * Legacy `@input` / `@change` component events → `@update:model-value`.
460
+ * When multiple source events map to the same target, the first is kept and
461
+ * subsequent duplicates are removed.
462
+ */
463
+ function applyEmitEventRenames (tag, canonical) {
464
+ const eventMap = EMIT_EVENT_RENAMES[canonical];
465
+ if (!eventMap) return { tag, count: 0 };
466
+ let result = tag;
467
+ let count = 0;
468
+ for (const [oldEvt, newEvt] of Object.entries(eventMap)) {
469
+ const before = result;
470
+ result = result.replace(new RegExp(`@${escapeRe(oldEvt)}\\b`, 'g'), `@${newEvt}`);
471
+ if (result !== before) count++;
472
+ }
473
+ const { tag: deduped, removedCount } = deduplicateEventBindings(result);
474
+ return { tag: deduped, count: count - removedCount };
475
+ }
476
+
477
+ /**
478
+ * Warn about removed rootClass / root-class prop on components not in ROOT_CLASS_RENAMES.
479
+ */
480
+ function detectRootClass (tag, tagName, canonical) {
481
+ if (ROOT_CLASS_RENAMES[canonical]) return [];
482
+ const re = /\broot-class(?:=|\b)|\brootClass(?:=|\b)/g;
483
+ const matches = tag.match(re);
484
+ if (!matches) return [];
485
+ return matches.map(() =>
486
+ `rootClass / root-class has been removed from <${tagName}>` +
487
+ ' — apply classes directly on the component element or use a wrapper.',
488
+ );
489
+ }
490
+
491
+ // ---------------------------------------------------------------------------
492
+ // Per-tag Entry Point
493
+ // ---------------------------------------------------------------------------
494
+
495
+ /**
496
+ * Returns the canonical kebab-case component name for config lookups.
497
+ * PascalCase → kebab-case: DtAvatar → dt-avatar
498
+ */
499
+ function normalizeTagName (tag) {
500
+ if (tag.startsWith('dt-')) return tag;
501
+ return tag.replace(/(?<=[a-z])([A-Z])/g, '-$1').toLowerCase();
502
+ }
503
+
504
+ function transformTag (fullTag, tagName) {
505
+ const canonical = normalizeTagName(tagName);
506
+ let tag = fullTag;
507
+ let count = 0;
508
+ const warnings = [];
509
+
510
+ const r1 = applyVModelAndEventRenames(tag, canonical);
511
+ tag = r1.tag; count += r1.count;
512
+
513
+ const r2 = applyShowToOpen(tag, canonical);
514
+ tag = r2.tag; count += r2.count;
515
+
516
+ const r3 = applyPropValueRenames(tag);
517
+ tag = r3.tag; count += r3.count;
518
+
519
+ const r4 = applyPropRenames(tag, canonical);
520
+ tag = r4.tag; count += r4.count;
521
+
522
+ const r5 = applyInvertedBoolProps(tag, canonical);
523
+ tag = r5.tag; count += r5.count;
524
+ warnings.push(...r5.warnings);
525
+
526
+ const r6 = applyRootClassRename(tag, canonical);
527
+ tag = r6.tag; count += r6.count;
528
+ warnings.push(...r6.warnings);
529
+
530
+ const r7 = applyEmitEventRenames(tag, canonical);
531
+ tag = r7.tag; count += r7.count;
532
+
533
+ warnings.push(...detectRootClass(tag, tagName, canonical));
534
+
535
+ return { result: tag, count, warnings };
536
+ }
537
+
538
+ // ---------------------------------------------------------------------------
539
+ // Full-file Slot Transforms
540
+ // ---------------------------------------------------------------------------
541
+
542
+ function transformSlots (content) {
543
+ let result = content;
544
+ let count = 0;
545
+
546
+ for (const { old: oldName, new: newName } of SLOT_RENAMES) {
547
+ const patterns = [
548
+ [new RegExp(`#${escapeRe(oldName)}\\b`, 'g'), `#${newName}`],
549
+ [new RegExp(`v-slot:${escapeRe(oldName)}\\b`, 'g'), `v-slot:${newName}`],
550
+ [new RegExp(`slot="${escapeRe(oldName)}"`, 'g'), `slot="${newName}"`],
551
+ ];
552
+
553
+ for (const [re, replacement] of patterns) {
554
+ const before = result;
555
+ result = result.replace(re, replacement);
556
+ if (result !== before) count += (before.match(re) || []).length;
557
+ }
558
+ }
559
+
560
+ return { result, count };
561
+ }
562
+
563
+ // ---------------------------------------------------------------------------
564
+ // Main Transform
565
+ // ---------------------------------------------------------------------------
566
+
567
+ function transformContent (content) {
568
+ let transformed = content;
569
+ let totalCount = 0;
570
+ const allWarnings = [];
571
+
572
+ const { result: slotResult, count: slotCount } = transformSlots(transformed);
573
+ transformed = slotResult;
574
+ totalCount += slotCount;
575
+
576
+ DT_TAG_RE.lastIndex = 0;
577
+ transformed = transformed.replace(DT_TAG_RE, (fullMatch, tagName) => {
578
+ const { result, count, warnings } = transformTag(fullMatch, tagName);
579
+ totalCount += count;
580
+ if (warnings.length) allWarnings.push(...warnings);
581
+ return result;
582
+ });
583
+
584
+ return { transformed, count: totalCount, warnings: allWarnings };
585
+ }
586
+
587
+ export { transformContent };
588
+
589
+ // ---------------------------------------------------------------------------
590
+ // File Finder
591
+ // ---------------------------------------------------------------------------
592
+
593
+ async function findFiles (dir, extensions, ignore = []) {
594
+ const results = [];
595
+
596
+ async function walk (currentDir) {
597
+ try {
598
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
599
+ for (const entry of entries) {
600
+ const fullPath = path.join(currentDir, entry.name);
601
+ if (ignore.some(ig => fullPath.includes(ig))) continue;
602
+ if (entry.isDirectory()) {
603
+ await walk(fullPath);
604
+ } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
605
+ results.push(fullPath);
606
+ }
607
+ }
608
+ } catch {
609
+ // skip unreadable dirs
610
+ }
611
+ }
612
+
613
+ await walk(dir);
614
+ return results;
615
+ }
616
+
617
+ // ---------------------------------------------------------------------------
618
+ // CLI
619
+ // ---------------------------------------------------------------------------
620
+
621
+ function printHelp () {
622
+ console.log(`
623
+ Usage: npx dialtone-migrate-props [options]
624
+
625
+ Migrates Dialtone component prop/slot/event breaking changes introduced in
626
+ DLT-3100, DLT-3157, DLT-3159, DLT-3160, DLT-3161, DLT-3282, DLT-3283, DLT-3284.
627
+
628
+ Migrations applied:
629
+ clickable → interactive (dt-avatar)
630
+ selected-values → model-value (dt-checkbox-group)
631
+ v-model:selected-values → v-model (dt-checkbox-group)
632
+ @update:selected-values → @update:model-value (dt-checkbox-group)
633
+ @input → @update:model-value (dt-input, dt-radio, dt-radio-group, dt-combobox-multi-select, dt-rich-text-editor, dt-input-group)
634
+ @change → @update:model-value (dt-toggle, dt-select-menu)
635
+ show → open (dt-modal, dt-toast, dt-tooltip)
636
+ @update:show → @update:open (dt-modal, dt-toast, dt-tooltip)
637
+ v-model:show → v-model:open (dt-modal, dt-toast, dt-tooltip)
638
+ title → header-text (dt-banner, dt-notice, dt-toast, dt-modal)
639
+ title-id → header-id (dt-banner, dt-notice, dt-toast)
640
+ banner-title → banner-header-text (dt-modal)
641
+ label-visible → show-label (dt-checkbox, dt-combobox, dt-input, etc.)
642
+ hide-close → :show-close="false" (dt-banner, dt-chip, dt-modal, dt-notice, dt-notice-action, dt-toast)
643
+ hide-icon → :show-icon="false" (dt-banner, dt-notice, dt-toast)
644
+ hide-action → :show-action="false" (dt-banner, dt-notice, dt-notice-action, dt-toast)
645
+ hide-clear → :show-clear="false" (dt-filter-pill)
646
+ hide-edges → :show-edges="false" (dt-pagination)
647
+ hide-divider → :show-divider="false" (dt-segmented-control)
648
+ prevent-typing → :allow-typing="false" (dt-rich-text-editor, dt-recipe-message-input)
649
+ hide-link-bubble-menu → :show-link-bubble-menu="false" (dt-rich-text-editor)
650
+ hide-actions → :show-actions="false" (dt-recipe-contact-centers-row)
651
+ hide-on-click → close-on-click (dt-popover, same semantics — not inverted)
652
+ root-class → class (dt-input, dt-checkbox, dt-radio, dt-select-menu, dt-breadcrumb-item, dt-split-button)
653
+ wrapper-class → class (dt-toggle, dt-feed-item-pill)
654
+ container-class → class (dt-card)
655
+ kind="danger" → kind="critical" (any dt-* component)
656
+ kind="error" → kind="critical" (any dt-* component)
657
+ kind="success" → kind="positive" (any dt-* component)
658
+ type="success" → type="positive" (dt-badge)
659
+ tone="success" → tone="positive" (dt-text, dt-link)
660
+ link-kind="danger" → link-kind="critical" (dt-button)
661
+ link-kind="success" → link-kind="positive" (dt-button)
662
+ kind → tone (dt-link, with value renames applied in same pass)
663
+ banner-kind="error" → banner-kind="critical" (dt-modal)
664
+ banner-kind="success" → banner-kind="positive" (dt-modal)
665
+ validation-state="error" → validation-state="critical"
666
+ validation-state="success" → validation-state="positive"
667
+ #titleOverride → #header (slots)
668
+ #labelSlot → #label (slots)
669
+ #headingSlot → #heading (slots)
670
+ rootClass / root-class → (warns — manual fix required for unknown components)
671
+
672
+ Options:
673
+ --cwd <path> Working directory (default: current directory)
674
+ --dry-run Show changes without applying them
675
+ --yes Apply all changes without prompting
676
+ --help Show help
677
+ `);
678
+ }
679
+
680
+ async function prompt (question) {
681
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
682
+ return new Promise(resolve => {
683
+ rl.question(question, answer => {
684
+ rl.close();
685
+ resolve(answer.trim().toLowerCase());
686
+ });
687
+ });
688
+ }
689
+
690
+ function parseArgs (args) {
691
+ const cwdIndex = args.indexOf('--cwd');
692
+ return {
693
+ help: args.includes('--help'),
694
+ dryRun: args.includes('--dry-run'),
695
+ autoYes: args.includes('--yes'),
696
+ cwd: cwdIndex !== -1 && args[cwdIndex + 1]
697
+ ? path.resolve(args[cwdIndex + 1])
698
+ : process.cwd(),
699
+ };
700
+ }
701
+
702
+ async function scanFiles (cwd) {
703
+ const extensions = ['.vue', '.md', '.html', '.js', '.ts', '.jsx', '.tsx'];
704
+ const ignore = ['node_modules', 'dist', '.git', '.vuepress/public'];
705
+ const files = await findFiles(cwd, extensions, ignore);
706
+
707
+ const changes = [];
708
+ const allWarnings = [];
709
+
710
+ for (const file of files) {
711
+ const content = await fs.readFile(file, 'utf8');
712
+ const { transformed, count, warnings } = transformContent(content);
713
+ if (count > 0) changes.push({ file, content, transformed, count });
714
+ if (warnings.length) allWarnings.push(...warnings.map(w => `${path.relative(cwd, file)}: ${w}`));
715
+ }
716
+
717
+ return { changes, allWarnings };
718
+ }
719
+
720
+ async function applyChanges (changes, autoYes) {
721
+ if (!autoYes) {
722
+ const answer = await prompt('\nApply changes? (y/N) ');
723
+ if (answer !== 'y' && answer !== 'yes') {
724
+ console.log('Cancelled.');
725
+ return false;
726
+ }
727
+ }
728
+ for (const { file, transformed } of changes) {
729
+ await fs.writeFile(file, transformed, 'utf8');
730
+ }
731
+ return true;
732
+ }
733
+
734
+ function printWarnings (warnings) {
735
+ if (!warnings.length) return;
736
+ console.log('⚠ Manual action required:\n');
737
+ for (const w of warnings) console.log(` ${w}`);
738
+ console.log();
739
+ }
740
+
741
+ function printChangeSummary (changes, cwd) {
742
+ const total = changes.reduce((sum, c) => sum + c.count, 0);
743
+ console.log(`Found ${total} change(s) across ${changes.length} file(s):\n`);
744
+ for (const { file, count } of changes) {
745
+ console.log(` ${path.relative(cwd, file)} (${count} change${count > 1 ? 's' : ''})`);
746
+ }
747
+ return total;
748
+ }
749
+
750
+ async function main () {
751
+ const opts = parseArgs(process.argv.slice(2));
752
+ if (opts.help) { printHelp(); process.exit(0); }
753
+
754
+ console.log(`\nScanning ${opts.cwd} for Dialtone prop/slot/event usage...\n`);
755
+
756
+ const { changes, allWarnings } = await scanFiles(opts.cwd);
757
+ printWarnings(allWarnings);
758
+
759
+ if (changes.length === 0 && allWarnings.length === 0) {
760
+ console.log('No matching usage found. Nothing to migrate.');
761
+ process.exit(0);
762
+ }
763
+
764
+ if (changes.length === 0) {
765
+ console.log('No automated code changes needed. See manual action items above.');
766
+ process.exit(0);
767
+ }
768
+
769
+ const total = printChangeSummary(changes, opts.cwd);
770
+
771
+ if (opts.dryRun) {
772
+ const warnNote = allWarnings.length ? ` ${allWarnings.length} warning(s) require manual action.` : '';
773
+ console.log(`\n--dry-run: No files were modified.${warnNote}\n`);
774
+ process.exit(0);
775
+ }
776
+
777
+ const applied = await applyChanges(changes, opts.autoYes);
778
+ if (applied) console.log(`\nMigrated ${total} reference(s) across ${changes.length} file(s).\n`);
779
+ }
780
+
781
+ const isDirectRun = (() => {
782
+ try {
783
+ return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
784
+ } catch {
785
+ return false;
786
+ }
787
+ })();
788
+
789
+ if (isDirectRun) {
790
+ main().catch(err => {
791
+ console.error(err);
792
+ process.exit(1);
793
+ });
794
+ }