@dialpad/dialtone-css 8.81.0-next.1 → 8.81.0-next.3

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 (24) hide show
  1. package/README.md +8 -0
  2. package/lib/build/js/dialtone_migrate/index.mjs +5 -3
  3. package/lib/build/js/dialtone_migrate_flex_to_stack/index.mjs +87 -5
  4. package/lib/build/js/dialtone_migrate_typography/index.mjs +116 -17
  5. package/lib/build/js/dialtone_migrate_typography/test.mjs +170 -0
  6. package/lib/build/js/dialtone_migration_helper/configs/space-to-spacing.mjs +6 -3
  7. package/lib/build/js/dialtone_migration_helper/configs/stack-gap-to-spacing.mjs +3 -0
  8. package/lib/build/js/dialtone_migration_helper/configs/utility-class-to-token-stops.mjs +24 -36
  9. package/lib/build/js/dialtone_migration_helper/tests/space-to-spacing-test-examples.vue +5 -9
  10. package/lib/build/js/dialtone_migration_helper/tests/utility-class-to-token-stops.test.mjs +22 -0
  11. package/lib/dist/js/dialtone_migrate/index.mjs +5 -3
  12. package/lib/dist/js/dialtone_migrate_flex_to_stack/index.mjs +87 -5
  13. package/lib/dist/js/dialtone_migrate_typography/index.mjs +116 -17
  14. package/lib/dist/js/dialtone_migrate_typography/test.mjs +170 -0
  15. package/lib/dist/js/dialtone_migration_helper/configs/space-to-spacing.mjs +6 -3
  16. package/lib/dist/js/dialtone_migration_helper/configs/stack-gap-to-spacing.mjs +3 -0
  17. package/lib/dist/js/dialtone_migration_helper/configs/utility-class-to-token-stops.mjs +24 -36
  18. package/lib/dist/js/dialtone_migration_helper/tests/space-to-spacing-test-examples.vue +5 -9
  19. package/lib/dist/js/dialtone_migration_helper/tests/utility-class-to-token-stops.test.mjs +22 -0
  20. package/lib/dist/no-layers/dialtone-default-theme.css +33541 -0
  21. package/lib/dist/no-layers/dialtone-default-theme.min.css +1 -0
  22. package/lib/dist/no-layers/dialtone.css +27809 -0
  23. package/lib/dist/no-layers/dialtone.min.css +1 -0
  24. package/package.json +10 -2
@@ -36,7 +36,7 @@ const NEGATIVE_SPACING_MAP = {
36
36
  32: '400', 48: '600', 64: '800',
37
37
  };
38
38
 
39
- // Layout sizes for margin/padding (96px, 128px use layout tokens)
39
+ // Layout sizes for sizing (96px, 128px use layout tokens)
40
40
  const SPACING_LAYOUT_MAP = {
41
41
  96: '150', 128: '200',
42
42
  };
@@ -77,8 +77,8 @@ export default {
77
77
  'Migrates pixel-based utility class names to token-stop-based names.\n' +
78
78
  '- Sizing: d-h16 → d-h-25, d-w64 → d-w-100, d-hmn96 → d-hmn-150\n' +
79
79
  '- Off-scale sizing: d-w1 → d-w-1px, d-h24 → d-h-24px (pixel-indexed exceptions)\n' +
80
- '- Margin: d-m8 → d-m-100, d-mt16 → d-mt-200, d-mtn8 → d-mt-n100\n' +
81
- '- Padding: d-p8 → d-p-100, d-pt16 → d-pt-200\n' +
80
+ '- Margin: d-m8 → d-m-100, d-mt16 → d-mt-200, d-mtn8 → d-mt-n100 (values ≥ 96px left unchanged)\n' +
81
+ '- Padding: d-p8 → d-p-100, d-pt16 → d-pt-200 (values ≥ 96px left unchanged — no layout-token padding class exists)\n' +
82
82
  '- Gap: d-g8 → d-g-100, d-rg16 → d-rg-200\n' +
83
83
  '- Position: d-t8 → d-t-100, d-tn8 → d-t-n100\n' +
84
84
  '- Border-radius all: d-bar6 → d-bar-350, d-bar24 → d-bar-550\n' +
@@ -134,18 +134,13 @@ export default {
134
134
  ]),
135
135
 
136
136
  // ── Margin: d-m{px} → d-m-{spacing-stop} ─────────────────────────────
137
- ...['m', 'mt', 'mr', 'mb', 'ml', 'mx', 'my'].flatMap(prefix => [
138
- // Spacing sizes (0-64px)
139
- {
140
- from: buildClassRegex(`d-${prefix}`, SPACING_MAP),
141
- to: (match, pre, px, post) => `${pre}d-${prefix}-${SPACING_MAP[px]}${post}`,
142
- },
143
- // Layout sizes (96, 128px)
144
- {
145
- from: buildClassRegex(`d-${prefix}`, SPACING_LAYOUT_MAP),
146
- to: (match, pre, px, post) => `${pre}d-${prefix}-${SPACING_LAYOUT_MAP[px]}${post}`,
147
- },
148
- ]),
137
+ // Only spacing-scale values (0-64px) are migrated. Values ≥ 96px (e.g. d-mt96)
138
+ // are left unchanged: token-stop margin classes use spacing tokens only, so
139
+ // d-mt-150 resolves to --dt-spacing-150 (12px), not --dt-layout-150 (96px).
140
+ ...['m', 'mt', 'mr', 'mb', 'ml', 'mx', 'my'].map(prefix => ({
141
+ from: buildClassRegex(`d-${prefix}`, SPACING_MAP),
142
+ to: (match, pre, px, post) => `${pre}d-${prefix}-${SPACING_MAP[px]}${post}`,
143
+ })),
149
144
 
150
145
  // ── Negative margin: d-m{dir}n{px} → d-m{dir}-n{spacing-stop} ────────
151
146
  ...['mt', 'mr', 'mb', 'ml', 'mx', 'my', 'm'].map(prefix => ({
@@ -154,16 +149,13 @@ export default {
154
149
  })),
155
150
 
156
151
  // ── Padding: d-p{px} → d-p-{spacing-stop} ────────────────────────────
157
- ...['p', 'pt', 'pr', 'pb', 'pl', 'px', 'py'].flatMap(prefix => [
158
- {
159
- from: buildClassRegex(`d-${prefix}`, SPACING_MAP),
160
- to: (match, pre, px, post) => `${pre}d-${prefix}-${SPACING_MAP[px]}${post}`,
161
- },
162
- {
163
- from: buildClassRegex(`d-${prefix}`, SPACING_LAYOUT_MAP),
164
- to: (match, pre, px, post) => `${pre}d-${prefix}-${SPACING_LAYOUT_MAP[px]}${post}`,
165
- },
166
- ]),
152
+ // Only spacing-scale values (0-64px) are migrated. Values ≥ 96px (e.g. d-pt96)
153
+ // are left unchanged: token-stop padding classes use spacing tokens only, so
154
+ // d-pt-150 resolves to --dt-spacing-150 (12px), not --dt-layout-150 (96px).
155
+ ...['p', 'pt', 'pr', 'pb', 'pl', 'px', 'py'].map(prefix => ({
156
+ from: buildClassRegex(`d-${prefix}`, SPACING_MAP),
157
+ to: (match, pre, px, post) => `${pre}d-${prefix}-${SPACING_MAP[px]}${post}`,
158
+ })),
167
159
 
168
160
  // ── Gap: d-g{px} → d-g-{spacing-stop} ────────────────────────────────
169
161
  ...['g', 'rg', 'cg'].map(prefix => ({
@@ -172,17 +164,13 @@ export default {
172
164
  })),
173
165
 
174
166
  // ── Position: d-t{px} → d-t-{spacing-stop} ───────────────────────────
175
- ...['t', 'r', 'b', 'l', 'x', 'y', 'all'].flatMap(prefix => [
176
- {
177
- from: buildClassRegex(`d-${prefix}`, SPACING_MAP),
178
- to: (match, pre, px, post) => `${pre}d-${prefix}-${SPACING_MAP[px]}${post}`,
179
- },
180
- // Layout position sizes (96px)
181
- {
182
- from: buildClassRegex(`d-${prefix}`, SPACING_LAYOUT_MAP),
183
- to: (match, pre, px, post) => `${pre}d-${prefix}-${SPACING_LAYOUT_MAP[px]}${post}`,
184
- },
185
- ]),
167
+ // Only spacing-scale values (0-64px) are migrated. Values ≥ 96px (e.g. d-t96)
168
+ // are left unchanged: token-stop position classes use spacing tokens only, so
169
+ // d-t-150 resolves to --dt-spacing-150 (12px), not --dt-layout-150 (96px).
170
+ ...['t', 'r', 'b', 'l', 'x', 'y', 'all'].map(prefix => ({
171
+ from: buildClassRegex(`d-${prefix}`, SPACING_MAP),
172
+ to: (match, pre, px, post) => `${pre}d-${prefix}-${SPACING_MAP[px]}${post}`,
173
+ })),
186
174
 
187
175
  // ── Negative position: d-{dir}n{px} → d-{dir}-n{spacing-stop} ────────
188
176
  ...['t', 'r', 'b', 'l', 'x', 'y', 'all'].map(prefix => ({
@@ -149,23 +149,19 @@
149
149
  }
150
150
 
151
151
  /* ============================================ */
152
- /* PERCENT VARIANTS */
152
+ /* SKIP CASES (should NOT be modified) */
153
153
  /* ============================================ */
154
154
 
155
- /* space-400-percent spacing-100-percent */
156
- .test-percent-400 {
155
+ /* -percent variants — no --dt-spacing-*-percent tokens exist; percent tokens live under
156
+ --dt-layout-*-percent with a different stop axis, so these are left for manual review */
157
+ .test-skip-percent-400 {
157
158
  padding: var(--dt-space-400-percent);
158
159
  }
159
160
 
160
- /* space-600-percent → spacing-400-percent */
161
- .test-percent-600 {
161
+ .test-skip-percent-600 {
162
162
  gap: var(--dt-space-600-percent);
163
163
  }
164
164
 
165
- /* ============================================ */
166
- /* SKIP CASES (should NOT be modified) */
167
- /* ============================================ */
168
-
169
165
  /* Stops with no --dt-spacing-* equivalent — leave unchanged */
170
166
  .test-skip-720 {
171
167
  padding: var(--dt-space-720); /* 72px — no spacing equivalent */
@@ -89,6 +89,28 @@ describe('utility-class-to-token-stops config', () => {
89
89
  });
90
90
  });
91
91
 
92
+
93
+ // ─── Large spacing values (≥ 96px) pass through unchanged ────────────
94
+ //
95
+ // Token-stop margin/padding/position classes (d-mt-{stop}, d-pt-{stop}, d-t-{stop})
96
+ // use spacing tokens only. d-mt-150 = --dt-spacing-150 (12px), not --dt-layout-150 (96px).
97
+ // These classes must be left for manual review rather than silently changed.
98
+
99
+ describe('large margin/padding/position values — pass through unchanged', () => {
100
+ const unchanged = [
101
+ 'd-mt96', 'd-mb96', 'd-ml96', 'd-mr96', 'd-mx96', 'd-my96', 'd-m96',
102
+ 'd-mt128', 'd-mb128', 'd-ml128', 'd-mr128', 'd-mx128', 'd-my128', 'd-m128',
103
+ 'd-pt96', 'd-pb96', 'd-pl96', 'd-pr96', 'd-px96', 'd-py96', 'd-p96',
104
+ 'd-pt128', 'd-pb128', 'd-pl128', 'd-pr128', 'd-px128', 'd-py128', 'd-p128',
105
+ 'd-t96', 'd-r96', 'd-b96', 'd-l96',
106
+ ];
107
+ for (const cls of unchanged) {
108
+ it(`${cls} is left unchanged`, () => {
109
+ assert.equal(apply(`<div class="${cls}" />`), `<div class="${cls}" />`);
110
+ });
111
+ }
112
+ });
113
+
92
114
  // ─── Border-radius migration (DLT-3329) ───────────────────────────────
93
115
 
94
116
  describe('border-radius all-corners — legacy pixel-suffix to token stop', () => {
@@ -363,6 +363,7 @@ function parseArgs (args) {
363
363
  help: args.includes('--help'),
364
364
  dryRun: args.includes('--dry-run'),
365
365
  autoYes: args.includes('--yes'),
366
+ noImport: args.includes('--no-import'),
366
367
  healthCheck: args.includes('--health-check'),
367
368
  all: args.includes('--all'),
368
369
  cwd: cwdIndex !== -1 && args[cwdIndex + 1]
@@ -742,8 +743,8 @@ async function runConfigMigration (migration, opts) {
742
743
  // Loop until convergence: some config regexes only match one token per
743
744
  // property declaration per pass (e.g. border-width with two var(--dt-size-*)).
744
745
  for (const entry of matched) {
745
- let changed = true;
746
- while (changed) {
746
+ let changed;
747
+ do {
747
748
  changed = false;
748
749
  for (const expr of configData.expressions) {
749
750
  const before = entry.data;
@@ -757,7 +758,7 @@ async function runConfigMigration (migration, opts) {
757
758
  changed = true;
758
759
  }
759
760
  }
760
- }
761
+ } while (changed && !configData.singlePass);
761
762
  if (entry.matches > 0) {
762
763
  await fs.writeFile(entry.file, entry.data, 'utf8');
763
764
  const shortname = path.relative(opts.cwd, entry.file);
@@ -778,6 +779,7 @@ async function runStandaloneMigration (migration, opts) {
778
779
  const args = ['--cwd', opts.cwd];
779
780
  if (opts.dryRun) args.push('--dry-run');
780
781
  if (opts.autoYes) args.push('--yes');
782
+ if (opts.noImport) args.push('--no-import');
781
783
 
782
784
  return new Promise((resolve, reject) => {
783
785
  const child = spawn(process.execPath, [scriptPath, ...args], {
@@ -1028,15 +1028,28 @@ async function processFile(filePath, options) {
1028
1028
  newContent = newContent.slice(0, r.start) + r.replacement + newContent.slice(r.end);
1029
1029
  }
1030
1030
 
1031
- await fs.writeFile(filePath, newContent, 'utf-8');
1032
- console.log(log.green(` ✓ Saved ${changes} change(s)`));
1031
+ // Auto-inject DtStack import before writing; fall back to manual instructions if needed.
1032
+ // Skipped entirely when --no-import is set (e.g. DtStack is globally registered).
1033
+ const importCheck = options.noImport ? null : detectMissingStackImport(newContent, changes > 0);
1034
+ let finalContent = newContent;
1035
+ if (importCheck?.needsImport) {
1036
+ const injected = injectComponentImport(newContent, 'DtStack', importCheck.suggestedPath);
1037
+ if (injected) finalContent = injected;
1038
+ }
1039
+
1040
+ await fs.writeFile(filePath, finalContent, 'utf-8');
1033
1041
 
1034
- // Check if file needs DtStack import
1035
- const importCheck = detectMissingStackImport(newContent, changes > 0);
1036
1042
  if (importCheck?.needsImport) {
1043
+ if (finalContent !== newContent) {
1044
+ console.log(log.green(` ✓ Saved ${changes} change(s) + added DtStack import`));
1045
+ return { changes, skipped, needsImport: false };
1046
+ }
1047
+ console.log(log.green(` ✓ Saved ${changes} change(s)`));
1037
1048
  printImportInstructions(filePath, importCheck);
1038
1049
  return { changes, skipped, needsImport: true };
1039
1050
  }
1051
+
1052
+ console.log(log.green(` ✓ Saved ${changes} change(s)`));
1040
1053
  }
1041
1054
 
1042
1055
  return { changes, skipped, needsImport: false };
@@ -1133,6 +1146,70 @@ function detectImportPattern(content) {
1133
1146
  return '@/components/stack';
1134
1147
  }
1135
1148
 
1149
+ /**
1150
+ * Attempt to auto-insert a component import (and Options API registration) into a Vue SFC.
1151
+ * Returns updated content on success, or null when the insertion can't be made safely
1152
+ * (caller should fall back to printing manual instructions).
1153
+ *
1154
+ * Handles:
1155
+ * - <script setup>: inserts import after the last existing import; no components
1156
+ * registration needed (auto-registered when imported in setup context).
1157
+ * - Options API with existing `components: {}`: inserts import + adds to the object.
1158
+ * - Options API without a components object: returns null (manual step required).
1159
+ */
1160
+ function injectComponentImport(content, componentName, importPath) {
1161
+ if (new RegExp(`import\\s+(?:\\{[^}]*\\b${componentName}\\b[^}]*\\}|${componentName})\\s+from`).test(content)) {
1162
+ return null; // already imported
1163
+ }
1164
+
1165
+ const isScriptSetup = /<script\b[^>]*\bsetup\b/.test(content);
1166
+ const scriptBlockRe = isScriptSetup
1167
+ ? /<script\b[^>]*\bsetup\b[^>]*>([\s\S]*?)<\/script>/
1168
+ : /<script\b(?![^>]*\bsetup\b)[^>]*>([\s\S]*?)<\/script>/;
1169
+ const scriptMatch = scriptBlockRe.exec(content);
1170
+ if (!scriptMatch) return null;
1171
+
1172
+ const scriptInnerStart = scriptMatch.index + scriptMatch[0].indexOf('>') + 1;
1173
+ const scriptInner = scriptMatch[1];
1174
+
1175
+ const importLineRe = /^import\s.+from\s+['"][^'"]+['"]\s*;?/gm;
1176
+ let lastImportMatch = null;
1177
+ let m;
1178
+ while ((m = importLineRe.exec(scriptInner)) !== null) lastImportMatch = m;
1179
+
1180
+ const insertOffset = lastImportMatch
1181
+ ? scriptInnerStart + lastImportMatch.index + lastImportMatch[0].length
1182
+ : scriptInnerStart;
1183
+
1184
+ const importLine = `\nimport { ${componentName} } from '${importPath}';`;
1185
+ let out = content.slice(0, insertOffset) + importLine + content.slice(insertOffset);
1186
+
1187
+ if (isScriptSetup) return out; // import alone is sufficient for <script setup>
1188
+
1189
+ // Options API: also register in components: {}
1190
+ // Re-exec against the updated content to get current positions, then restrict
1191
+ // the components: { search to within the script block and after export default
1192
+ // to avoid matching template bindings or helper objects that appear earlier.
1193
+ const scriptMatchUpdated = scriptBlockRe.exec(out);
1194
+ if (!scriptMatchUpdated) return null;
1195
+ const scriptBodyStart = scriptMatchUpdated.index + scriptMatchUpdated[0].indexOf('>') + 1;
1196
+ const scriptBodyText = scriptMatchUpdated[1];
1197
+ const exportDefaultMatch = /\bexport\s+default\b/.exec(scriptBodyText);
1198
+ if (!exportDefaultMatch) return null;
1199
+ const searchFrom = scriptBodyStart + exportDefaultMatch.index;
1200
+ const compMatchInSlice = /components\s*:\s*\{/.exec(out.slice(searchFrom));
1201
+ if (!compMatchInSlice) return null; // no components object — can't safely auto-register
1202
+ const compAbsIndex = searchFrom + compMatchInSlice.index;
1203
+
1204
+ const lineStart = out.lastIndexOf('\n', compAbsIndex) + 1;
1205
+ const compIndent = out.slice(lineStart, compAbsIndex).match(/^[ \t]*/)[0];
1206
+ const memberIndent = compIndent + ' ';
1207
+ const insertAt = compAbsIndex + compMatchInSlice[0].length;
1208
+ out = out.slice(0, insertAt) + `\n${memberIndent}${componentName},` + out.slice(insertAt);
1209
+
1210
+ return out;
1211
+ }
1212
+
1136
1213
  /**
1137
1214
  * Print instructions for adding DtStack import and registration
1138
1215
  * @param {string} filePath - Path to the file
@@ -1179,6 +1256,7 @@ function parseArgs() {
1179
1256
  files: [], // Explicit file list via --file flag
1180
1257
  showOutline: false, // Add migration marker for visual debugging
1181
1258
  removeOutline: false, // Remove migration markers (cleanup mode)
1259
+ noImport: false, // Skip import injection (for apps that globally register DtStack)
1182
1260
  };
1183
1261
 
1184
1262
  for (let i = 0; i < args.length; i++) {
@@ -1206,11 +1284,12 @@ Options:
1206
1284
  --yes, -y Apply all changes without prompting
1207
1285
  --show-outline Add data-migrate-outline attribute for visual debugging
1208
1286
  --remove-outline Remove data-migrate-outline attributes after review
1287
+ --no-import Skip import injection (use when DtStack is globally registered)
1209
1288
  --help, -h Show help
1210
1289
 
1211
1290
  Post-Migration Steps:
1212
1291
  1. Review template changes with data-migrate-outline markers
1213
- 2. Add DtStack imports as instructed by the script
1292
+ 2. Add DtStack imports as instructed by the script (skipped with --no-import)
1214
1293
  3. Test your application
1215
1294
  4. Run with --remove-outline to clean up markers
1216
1295
 
@@ -1252,6 +1331,8 @@ Examples:
1252
1331
  options.showOutline = true;
1253
1332
  } else if (arg === '--remove-outline') {
1254
1333
  options.removeOutline = true;
1334
+ } else if (arg === '--no-import') {
1335
+ options.noImport = true;
1255
1336
  } else if (arg === '--file' && args[i + 1]) {
1256
1337
  const filePath = args[++i];
1257
1338
  options.files.push(filePath);
@@ -1348,6 +1429,7 @@ async function main() {
1348
1429
  yes: options.yes,
1349
1430
  showOutline: options.showOutline,
1350
1431
  validate: options.validate,
1432
+ noImport: options.noImport,
1351
1433
  });
1352
1434
  }
1353
1435
 
@@ -1002,17 +1002,31 @@ function flagDynamicClasses (content) {
1002
1002
  const re = new RegExp(DYNAMIC_CLASS_PATTERN);
1003
1003
  if (!re.test(content)) return content;
1004
1004
 
1005
- const lines = content.split('\n');
1006
- const out = lines.map(line => {
1007
- if (!/:class=|v-bind:class=/.test(line)) return line;
1008
- if (!new RegExp(DYNAMIC_CLASS_PATTERN).test(line)) return line;
1009
- // Preserve the line's indentation; put the marker on its own line above.
1010
- const indentMatch = line.match(/^[ \t]*/);
1011
- const indent = indentMatch ? indentMatch[0] : '';
1012
- return `${indent}<!-- dt-text-migrate: review dynamic class -->\n${line}`;
1013
- });
1005
+ // Walk whole opening tags (quote-aware) so the marker is never injected between
1006
+ // attributes of a multi-line element. The old line-by-line approach would insert
1007
+ // the comment before the :class= line, which lands it between attributes when the
1008
+ // opening tag spans multiple lines.
1009
+ const tagRe = new RegExp(`<[a-zA-Z][\\w-]*(${ATTR_BODY})>`, 'g');
1010
+ const insertions = [];
1011
+ let m;
1014
1012
 
1015
- return out.join('\n');
1013
+ while ((m = tagRe.exec(content)) !== null) {
1014
+ if (!new RegExp(DYNAMIC_CLASS_PATTERN).test(m[0])) continue;
1015
+ const before = content.slice(Math.max(0, m.index - 80), m.index);
1016
+ if (/<!--\s*dt-text-migrate:\s*review dynamic class\s*-->/.test(before)) continue;
1017
+ insertions.push(m.index);
1018
+ }
1019
+
1020
+ if (insertions.length === 0) return content;
1021
+
1022
+ let out = content;
1023
+ for (const idx of insertions.reverse()) {
1024
+ const lineStart = out.lastIndexOf('\n', idx - 1) + 1;
1025
+ const indent = out.slice(lineStart, idx).match(/^[ \t]*/)[0];
1026
+ const marker = `${indent}<!-- dt-text-migrate: review dynamic class -->\n`;
1027
+ out = out.slice(0, lineStart) + marker + out.slice(lineStart);
1028
+ }
1029
+ return out;
1016
1030
  }
1017
1031
 
1018
1032
  // Find composed typography classes on tags outside the rewrite scope
@@ -1245,6 +1259,70 @@ function detectMissingDtTextImport (content, usesText) {
1245
1259
  };
1246
1260
  }
1247
1261
 
1262
+ /**
1263
+ * Attempt to auto-insert a component import (and Options API registration) into a Vue SFC.
1264
+ * Returns updated content on success, or null when the insertion can't be made safely
1265
+ * (caller should fall back to printing manual instructions).
1266
+ *
1267
+ * Handles:
1268
+ * - <script setup>: inserts import after the last existing import; no components
1269
+ * registration needed (auto-registered when imported in setup context).
1270
+ * - Options API with existing `components: {}`: inserts import + adds to the object.
1271
+ * - Options API without a components object: returns null (manual step required).
1272
+ */
1273
+ export function injectComponentImport (content, componentName, importPath) {
1274
+ if (new RegExp(`import\\s+(?:\\{[^}]*\\b${componentName}\\b[^}]*\\}|${componentName})\\s+from`).test(content)) {
1275
+ return null; // already imported
1276
+ }
1277
+
1278
+ const isScriptSetup = /<script\b[^>]*\bsetup\b/.test(content);
1279
+ const scriptBlockRe = isScriptSetup
1280
+ ? /<script\b[^>]*\bsetup\b[^>]*>([\s\S]*?)<\/script>/
1281
+ : /<script\b(?![^>]*\bsetup\b)[^>]*>([\s\S]*?)<\/script>/;
1282
+ const scriptMatch = scriptBlockRe.exec(content);
1283
+ if (!scriptMatch) return null;
1284
+
1285
+ const scriptInnerStart = scriptMatch.index + scriptMatch[0].indexOf('>') + 1;
1286
+ const scriptInner = scriptMatch[1];
1287
+
1288
+ const importLineRe = /^import\s.+from\s+['"][^'"]+['"]\s*;?/gm;
1289
+ let lastImportMatch = null;
1290
+ let m;
1291
+ while ((m = importLineRe.exec(scriptInner)) !== null) lastImportMatch = m;
1292
+
1293
+ const insertOffset = lastImportMatch
1294
+ ? scriptInnerStart + lastImportMatch.index + lastImportMatch[0].length
1295
+ : scriptInnerStart;
1296
+
1297
+ const importLine = `\nimport { ${componentName} } from '${importPath}';`;
1298
+ let out = content.slice(0, insertOffset) + importLine + content.slice(insertOffset);
1299
+
1300
+ if (isScriptSetup) return out; // import alone is sufficient for <script setup>
1301
+
1302
+ // Options API: also register in components: {}
1303
+ // Re-exec against the updated content to get current positions, then restrict
1304
+ // the components: { search to within the script block and after export default
1305
+ // to avoid matching template bindings or helper objects that appear earlier.
1306
+ const scriptMatchUpdated = scriptBlockRe.exec(out);
1307
+ if (!scriptMatchUpdated) return null;
1308
+ const scriptBodyStart = scriptMatchUpdated.index + scriptMatchUpdated[0].indexOf('>') + 1;
1309
+ const scriptBodyText = scriptMatchUpdated[1];
1310
+ const exportDefaultMatch = /\bexport\s+default\b/.exec(scriptBodyText);
1311
+ if (!exportDefaultMatch) return null;
1312
+ const searchFrom = scriptBodyStart + exportDefaultMatch.index;
1313
+ const compMatchInSlice = /components\s*:\s*\{/.exec(out.slice(searchFrom));
1314
+ if (!compMatchInSlice) return null; // no components object — can't safely auto-register
1315
+ const compAbsIndex = searchFrom + compMatchInSlice.index;
1316
+
1317
+ const lineStart = out.lastIndexOf('\n', compAbsIndex) + 1;
1318
+ const compIndent = out.slice(lineStart, compAbsIndex).match(/^[ \t]*/)[0];
1319
+ const memberIndent = compIndent + ' ';
1320
+ const insertAt = compAbsIndex + compMatchInSlice[0].length;
1321
+ out = out.slice(0, insertAt) + `\n${memberIndent}${componentName},` + out.slice(insertAt);
1322
+
1323
+ return out;
1324
+ }
1325
+
1248
1326
  function printImportInstructions (filePath, importCheck) {
1249
1327
  console.log(log.yellow('\n⚠️ ACTION REQUIRED: Add DtText import and registration'));
1250
1328
  log.cyan(` File: ${filePath}`);
@@ -1450,9 +1528,6 @@ async function processFile (filePath, options) {
1450
1528
 
1451
1529
  if (!shouldApply) return { changes: 0, needsImport: false };
1452
1530
 
1453
- await fs.writeFile(filePath, transformed, 'utf-8');
1454
- console.log(log.green(' ✓ Saved'));
1455
-
1456
1531
  // Only warn about missing DtText import when we actually INSERTED new <dt-text> elements
1457
1532
  // — review-marker-only changes (eyebrow/d-code--sm/d-fs-* flags) don't require an import.
1458
1533
  // Use a count delta (not boolean presence) so partial migrations on a file that already
@@ -1460,14 +1535,34 @@ async function processFile (filePath, options) {
1460
1535
  const beforeCount = (content.match(/<dt-text\b/g) || []).length;
1461
1536
  const afterCount = (transformed.match(/<dt-text\b/g) || []).length;
1462
1537
  const addedDtText = afterCount > beforeCount;
1463
- const importCheck = detectMissingDtTextImport(transformed, addedDtText);
1464
- if (importCheck?.needsImport) printImportInstructions(filePath, importCheck);
1538
+ // Auto-inject DtText import before writing; fall back to manual instructions if needed.
1539
+ // Skipped entirely when --no-import is set (e.g. DtText is globally registered).
1540
+ const importCheck = options.noImport ? null : detectMissingDtTextImport(transformed, addedDtText);
1541
+
1542
+ let finalContent = transformed;
1543
+ if (importCheck?.needsImport) {
1544
+ const injected = injectComponentImport(transformed, 'DtText', importCheck.suggestedPath);
1545
+ if (injected) finalContent = injected;
1546
+ }
1547
+
1548
+ await fs.writeFile(filePath, finalContent, 'utf-8');
1549
+
1550
+ if (importCheck?.needsImport) {
1551
+ if (finalContent !== transformed) {
1552
+ console.log(log.green(' ✓ Saved + added DtText import'));
1553
+ } else {
1554
+ console.log(log.green(' ✓ Saved'));
1555
+ printImportInstructions(filePath, importCheck);
1556
+ }
1557
+ } else {
1558
+ console.log(log.green(' ✓ Saved'));
1559
+ }
1465
1560
 
1466
1561
  if (notes.length > 0) {
1467
1562
  for (const note of notes) log.gray(` ℹ ${note}`);
1468
1563
  }
1469
1564
 
1470
- return { changes: 1, needsImport: !!importCheck?.needsImport };
1565
+ return { changes: 1, needsImport: importCheck?.needsImport && finalContent === transformed };
1471
1566
  }
1472
1567
 
1473
1568
  //------------------------------------------------------------------------------
@@ -1484,6 +1579,7 @@ function parseArgs () {
1484
1579
  files: [],
1485
1580
  removeMarkers: false,
1486
1581
  validate: false,
1582
+ noImport: false, // Skip import injection (for apps that globally register DtText)
1487
1583
  };
1488
1584
 
1489
1585
  for (let i = 0; i < args.length; i++) {
@@ -1503,6 +1599,7 @@ Options:
1503
1599
  --remove-markers Strip all <!-- dt-text-migrate: review ... --> comments
1504
1600
  --validate Read-only mode: scan existing <dt-text> for prop bugs
1505
1601
  (object syntax, invalid values, mixed CSS classes)
1602
+ --no-import Skip import injection (use when DtText is globally registered)
1506
1603
  --help, -h Show help
1507
1604
 
1508
1605
  Examples:
@@ -1512,7 +1609,7 @@ Examples:
1512
1609
  npx dialtone-migrate-typography --remove-markers --cwd ./src
1513
1610
 
1514
1611
  Post-Migration Steps:
1515
- 1. Add DtText imports as instructed by the script
1612
+ 1. Add DtText imports as instructed by the script (skipped with --no-import)
1516
1613
  2. Review files marked with <!-- dt-text-migrate: review --> comments
1517
1614
  3. Run with --remove-markers to clean up all markers after manual review
1518
1615
  `);
@@ -1531,6 +1628,8 @@ Post-Migration Steps:
1531
1628
  options.removeMarkers = true;
1532
1629
  } else if (arg === '--validate') {
1533
1630
  options.validate = true;
1631
+ } else if (arg === '--no-import') {
1632
+ options.noImport = true;
1534
1633
  }
1535
1634
  }
1536
1635