@dialpad/dialtone-css 8.80.0-next.6 → 8.80.0-next.7

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 (134) hide show
  1. package/lib/build/js/dialtone_migrate_border_radius/index.mjs +273 -0
  2. package/lib/build/js/dialtone_migrate_border_radius/test.mjs +422 -0
  3. package/lib/build/js/dialtone_migrate_typography/index.mjs +1628 -0
  4. package/lib/build/js/dialtone_migrate_typography/test.mjs +1020 -0
  5. package/lib/build/js/dialtone_migration_helper/configs/theme-to-mode.mjs +108 -0
  6. package/lib/build/js/dialtone_migration_helper/tests/theme-to-mode-test-examples.vue +24 -0
  7. package/lib/build/js/dialtone_migration_helper/tests/theme-to-mode.test.mjs +177 -0
  8. package/lib/build/less/components/button.less +2 -0
  9. package/lib/build/less/components/emoji-picker.less +10 -11
  10. package/lib/build/less/components/forms.less +22 -16
  11. package/lib/build/less/components/modal.less +8 -2
  12. package/lib/build/less/components/notice.less +4 -0
  13. package/lib/build/less/components/popover.less +1 -1
  14. package/lib/build/less/components/presence.less +23 -3
  15. package/lib/build/less/recipes/leftbar_row.less +1 -0
  16. package/lib/dist/dialtone-default-theme.css +67 -34
  17. package/lib/dist/dialtone-default-theme.min.css +1 -1
  18. package/lib/dist/dialtone-docs.json +1 -1
  19. package/lib/dist/dialtone.css +66 -34
  20. package/lib/dist/dialtone.min.css +1 -1
  21. package/lib/dist/js/dialtone_migrate_border_radius/index.mjs +273 -0
  22. package/lib/dist/js/dialtone_migrate_border_radius/test.mjs +422 -0
  23. package/lib/dist/js/dialtone_migrate_typography/index.mjs +1628 -0
  24. package/lib/dist/js/dialtone_migrate_typography/test.mjs +1020 -0
  25. package/lib/dist/js/dialtone_migration_helper/configs/theme-to-mode.mjs +108 -0
  26. package/lib/dist/js/dialtone_migration_helper/tests/theme-to-mode-test-examples.vue +24 -0
  27. package/lib/dist/js/dialtone_migration_helper/tests/theme-to-mode.test.mjs +177 -0
  28. package/lib/dist/tokens/tokens-101-dark.css +1 -0
  29. package/lib/dist/tokens/tokens-101-light.css +1 -0
  30. package/lib/dist/tokens/tokens-102-dark.css +1 -0
  31. package/lib/dist/tokens/tokens-102-light.css +1 -0
  32. package/lib/dist/tokens/tokens-103-dark.css +1 -0
  33. package/lib/dist/tokens/tokens-103-light.css +1 -0
  34. package/lib/dist/tokens/tokens-104-dark.css +1 -0
  35. package/lib/dist/tokens/tokens-104-light.css +1 -0
  36. package/lib/dist/tokens/tokens-105-dark.css +1 -0
  37. package/lib/dist/tokens/tokens-105-light.css +1 -0
  38. package/lib/dist/tokens/tokens-106-dark.css +1 -0
  39. package/lib/dist/tokens/tokens-106-light.css +1 -0
  40. package/lib/dist/tokens/tokens-107-dark.css +1 -0
  41. package/lib/dist/tokens/tokens-107-light.css +1 -0
  42. package/lib/dist/tokens/tokens-108-dark.css +1 -0
  43. package/lib/dist/tokens/tokens-108-light.css +1 -0
  44. package/lib/dist/tokens/tokens-109-dark.css +1 -0
  45. package/lib/dist/tokens/tokens-109-light.css +1 -0
  46. package/lib/dist/tokens/tokens-110-dark.css +1 -0
  47. package/lib/dist/tokens/tokens-110-light.css +1 -0
  48. package/lib/dist/tokens/tokens-111-dark.css +1 -0
  49. package/lib/dist/tokens/tokens-111-light.css +1 -0
  50. package/lib/dist/tokens/tokens-112-dark.css +1 -0
  51. package/lib/dist/tokens/tokens-112-light.css +1 -0
  52. package/lib/dist/tokens/tokens-113-dark.css +1 -0
  53. package/lib/dist/tokens/tokens-113-light.css +1 -0
  54. package/lib/dist/tokens/tokens-114-dark.css +1 -0
  55. package/lib/dist/tokens/tokens-114-light.css +1 -0
  56. package/lib/dist/tokens/tokens-115-dark.css +1 -0
  57. package/lib/dist/tokens/tokens-115-light.css +1 -0
  58. package/lib/dist/tokens/tokens-116-dark.css +1 -0
  59. package/lib/dist/tokens/tokens-116-light.css +1 -0
  60. package/lib/dist/tokens/tokens-117-dark.css +1 -0
  61. package/lib/dist/tokens/tokens-117-light.css +1 -0
  62. package/lib/dist/tokens/tokens-118-dark.css +1 -0
  63. package/lib/dist/tokens/tokens-118-light.css +1 -0
  64. package/lib/dist/tokens/tokens-119-dark.css +1 -0
  65. package/lib/dist/tokens/tokens-119-light.css +1 -0
  66. package/lib/dist/tokens/tokens-120-dark.css +1 -0
  67. package/lib/dist/tokens/tokens-120-light.css +1 -0
  68. package/lib/dist/tokens/tokens-121-dark.css +1 -0
  69. package/lib/dist/tokens/tokens-121-light.css +1 -0
  70. package/lib/dist/tokens/tokens-122-dark.css +1 -0
  71. package/lib/dist/tokens/tokens-122-light.css +1 -0
  72. package/lib/dist/tokens/tokens-123-dark.css +1 -0
  73. package/lib/dist/tokens/tokens-123-light.css +1 -0
  74. package/lib/dist/tokens/tokens-124-dark.css +1 -0
  75. package/lib/dist/tokens/tokens-124-light.css +1 -0
  76. package/lib/dist/tokens/tokens-125-dark.css +1 -0
  77. package/lib/dist/tokens/tokens-125-light.css +1 -0
  78. package/lib/dist/tokens/tokens-126-dark.css +1 -0
  79. package/lib/dist/tokens/tokens-126-light.css +1 -0
  80. package/lib/dist/tokens/tokens-127-dark.css +1 -0
  81. package/lib/dist/tokens/tokens-127-light.css +1 -0
  82. package/lib/dist/tokens/tokens-128-dark.css +1 -0
  83. package/lib/dist/tokens/tokens-128-light.css +1 -0
  84. package/lib/dist/tokens/tokens-129-dark.css +1 -0
  85. package/lib/dist/tokens/tokens-129-light.css +1 -0
  86. package/lib/dist/tokens/tokens-130-dark.css +1 -0
  87. package/lib/dist/tokens/tokens-130-light.css +1 -0
  88. package/lib/dist/tokens/tokens-131-dark.css +1 -0
  89. package/lib/dist/tokens/tokens-131-light.css +1 -0
  90. package/lib/dist/tokens/tokens-132-dark.css +1 -0
  91. package/lib/dist/tokens/tokens-132-light.css +1 -0
  92. package/lib/dist/tokens/tokens-133-dark.css +1 -0
  93. package/lib/dist/tokens/tokens-133-light.css +1 -0
  94. package/lib/dist/tokens/tokens-134-dark.css +1 -0
  95. package/lib/dist/tokens/tokens-134-light.css +1 -0
  96. package/lib/dist/tokens/tokens-135-dark.css +1 -0
  97. package/lib/dist/tokens/tokens-135-light.css +1 -0
  98. package/lib/dist/tokens/tokens-136-dark.css +1 -0
  99. package/lib/dist/tokens/tokens-136-light.css +1 -0
  100. package/lib/dist/tokens/tokens-137-dark.css +1 -0
  101. package/lib/dist/tokens/tokens-137-light.css +1 -0
  102. package/lib/dist/tokens/tokens-aegean-dark.css +1 -0
  103. package/lib/dist/tokens/tokens-aegean-light.css +1 -0
  104. package/lib/dist/tokens/tokens-botany-dark.css +1 -0
  105. package/lib/dist/tokens/tokens-botany-light.css +1 -0
  106. package/lib/dist/tokens/tokens-buttercream-dark.css +1 -0
  107. package/lib/dist/tokens/tokens-buttercream-light.css +1 -0
  108. package/lib/dist/tokens/tokens-ceruleo-dark.css +1 -0
  109. package/lib/dist/tokens/tokens-ceruleo-light.css +1 -0
  110. package/lib/dist/tokens/tokens-debug-dp.css +1 -0
  111. package/lib/dist/tokens/tokens-dp-dark.css +1 -0
  112. package/lib/dist/tokens/tokens-dp-light.css +1 -0
  113. package/lib/dist/tokens/tokens-expressive-dark.css +1 -0
  114. package/lib/dist/tokens/tokens-expressive-light.css +1 -0
  115. package/lib/dist/tokens/tokens-expressive-sm-dark.css +1 -0
  116. package/lib/dist/tokens/tokens-expressive-sm-light.css +1 -0
  117. package/lib/dist/tokens/tokens-high-desert-dark.css +1 -0
  118. package/lib/dist/tokens/tokens-high-desert-light.css +1 -0
  119. package/lib/dist/tokens/tokens-melon-dark.css +1 -0
  120. package/lib/dist/tokens/tokens-melon-light.css +1 -0
  121. package/lib/dist/tokens/tokens-plum-dark.css +1 -0
  122. package/lib/dist/tokens/tokens-plum-light.css +1 -0
  123. package/lib/dist/tokens/tokens-prota-deuter-dark.css +1 -0
  124. package/lib/dist/tokens/tokens-prota-deuter-light.css +1 -0
  125. package/lib/dist/tokens/tokens-sunflower-dark.css +1 -0
  126. package/lib/dist/tokens/tokens-sunflower-light.css +1 -0
  127. package/lib/dist/tokens/tokens-tmo-dark.css +1 -0
  128. package/lib/dist/tokens/tokens-tmo-light.css +1 -0
  129. package/lib/dist/tokens/tokens-trita-dark.css +1 -0
  130. package/lib/dist/tokens/tokens-trita-light.css +1 -0
  131. package/lib/dist/tokens/tokens-verdant-haze-dark.css +1 -0
  132. package/lib/dist/tokens/tokens-verdant-haze-light.css +1 -0
  133. package/lib/dist/tokens-docs.json +1 -1
  134. package/package.json +5 -3
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @fileoverview Migration script for deprecated physical border-radius utility classes.
5
+ *
6
+ * DLT-3329 Physical directional border-radius utility classes have been replaced
7
+ * by logical equivalents. This script rewrites:
8
+ *
9
+ * All-corners numeric: d-bar6 → d-bar-350
10
+ * Pair numeric: d-btr8 → d-bbsr-400
11
+ * Pair keyword: d-btr-pill → d-bbsr-pill
12
+ *
13
+ * The full pair-prefix mapping is:
14
+ * btr → bbsr (top → block-start pair)
15
+ * bbr → bber (bottom → block-end pair)
16
+ * blr → bisr (left → inline-start pair)
17
+ * brr → bier (right → inline-end pair)
18
+ *
19
+ * Usage:
20
+ * npx dialtone-migrate-border-radius [options]
21
+ *
22
+ * Options:
23
+ * --cwd <path> Working directory (default: current directory)
24
+ * --dry-run Show changes without applying them
25
+ * --yes Apply all changes without prompting
26
+ * --help Show help
27
+ *
28
+ * Examples:
29
+ * npx dialtone-migrate-border-radius
30
+ * npx dialtone-migrate-border-radius --dry-run
31
+ * npx dialtone-migrate-border-radius --cwd ./src
32
+ * npx dialtone-migrate-border-radius --yes
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
+ // Mapping — MUST STAY IN SYNC with:
43
+ // - packages/eslint-plugin-dialtone/lib/rules/deprecated-radius-utility-classes.js
44
+ // - packages/dialtone-css/postcss/constants.cjs (RADIUS_STOPS)
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const RADIUS_STOP_MAP = {
48
+ 0: '0', 1: '100', 2: '200', 4: '300', 6: '350',
49
+ 8: '400', 12: '450', 16: '500', 24: '550', 32: '600',
50
+ };
51
+
52
+ const PAIR_PREFIX_MAP = {
53
+ btr: 'bbsr', // top → block-start pair
54
+ bbr: 'bber', // bottom → block-end pair
55
+ blr: 'bisr', // left → inline-start pair
56
+ brr: 'bier', // right → inline-end pair
57
+ };
58
+
59
+ // Ordered by descending string length so regex alternation matches longest first.
60
+ const NUMERIC_SUFFIXES = Object.keys(RADIUS_STOP_MAP)
61
+ .sort((a, b) => b.length - a.length || Number(b) - Number(a))
62
+ .join('|');
63
+ const PAIR_PREFIXES = Object.keys(PAIR_PREFIX_MAP).join('|');
64
+
65
+ // Word-boundary anchored patterns to avoid matching inside unrelated class names.
66
+ const ALL_CORNERS_NUMERIC = new RegExp(`(?<=[\\s"'=\`])d-bar(${NUMERIC_SUFFIXES})(?=[\\s"'>;\`])`, 'g');
67
+ const PAIR_NUMERIC = new RegExp(`(?<=[\\s"'=\`])d-(${PAIR_PREFIXES})(${NUMERIC_SUFFIXES})(?=[\\s"'>;\`])`, 'g');
68
+ const PAIR_KEYWORD = new RegExp(`(?<=[\\s"'=\`])d-(${PAIR_PREFIXES})-(pill|circle)(?=[\\s"'>;\`])`, 'g');
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Transform
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export function transformContent (content) {
75
+ let transformed = content;
76
+ let count = 0;
77
+
78
+ transformed = transformed
79
+ .replace(ALL_CORNERS_NUMERIC, (_, px) => { count++; return `d-bar-${RADIUS_STOP_MAP[px]}`; })
80
+ .replace(PAIR_NUMERIC, (_, legacyPrefix, px) => { count++; return `d-${PAIR_PREFIX_MAP[legacyPrefix]}-${RADIUS_STOP_MAP[px]}`; })
81
+ .replace(PAIR_KEYWORD, (_, legacyPrefix, keyword) => { count++; return `d-${PAIR_PREFIX_MAP[legacyPrefix]}-${keyword}`; });
82
+
83
+ return { transformed, count };
84
+ }
85
+
86
+ export { RADIUS_STOP_MAP, PAIR_PREFIX_MAP };
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // File walker
90
+ // ---------------------------------------------------------------------------
91
+
92
+ function isIgnoredPath (fullPath, ignore) {
93
+ const segments = fullPath.split(path.sep);
94
+ return ignore.some(ig => {
95
+ if (ig.includes('/')) {
96
+ const parts = ig.split('/');
97
+ for (let i = 0; i + parts.length <= segments.length; i++) {
98
+ if (parts.every((p, j) => segments[i + j] === p)) return true;
99
+ }
100
+ return false;
101
+ }
102
+ return segments.includes(ig);
103
+ });
104
+ }
105
+
106
+ async function findFiles (dir, extensions, ignore = []) {
107
+ const results = [];
108
+ async function walk (currentDir) {
109
+ let entries;
110
+ try {
111
+ entries = await fs.readdir(currentDir, { withFileTypes: true });
112
+ } catch {
113
+ return;
114
+ }
115
+ for (const entry of entries) {
116
+ const fullPath = path.join(currentDir, entry.name);
117
+ if (isIgnoredPath(fullPath, ignore)) continue;
118
+ if (entry.isDirectory()) {
119
+ await walk(fullPath);
120
+ } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
121
+ results.push(fullPath);
122
+ }
123
+ }
124
+ }
125
+ await walk(dir);
126
+ return results;
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // CLI
131
+ // ---------------------------------------------------------------------------
132
+
133
+ function printHelp () {
134
+ console.log(`
135
+ Usage: npx dialtone-migrate-border-radius [options]
136
+
137
+ Replaces deprecated physical border-radius utility classes with logical equivalents (DLT-3329).
138
+
139
+ d-bar6 → d-bar-350 (all-corners numeric → token stop)
140
+ d-btr8 → d-bbsr-400 (top pair → block-start pair)
141
+ d-bbr-pill → d-bber-pill (bottom pair → block-end pair)
142
+ d-blr12 → d-bisr-450 (left pair → inline-start pair)
143
+ d-brr-circle → d-bier-circle (right pair → inline-end pair)
144
+
145
+ Pair-prefix mapping:
146
+ btr → bbsr (top → block-start)
147
+ bbr → bber (bottom → block-end)
148
+ blr → bisr (left → inline-start)
149
+ brr → bier (right → inline-end)
150
+
151
+ Numeric-stop mapping:
152
+ 0 → 0 1 → 100 2 → 200 4 → 300 6 → 350
153
+ 8 → 400 12 → 450 16 → 500 24 → 550 32 → 600
154
+
155
+ Options:
156
+ --cwd <path> Working directory (default: current directory)
157
+ --dry-run Show changes without applying them
158
+ --yes Apply all changes without prompting
159
+ --help Show help
160
+ `);
161
+ }
162
+
163
+ function parseArgs (args) {
164
+ const cwdIndex = args.indexOf('--cwd');
165
+ let cwd = process.cwd();
166
+ if (cwdIndex !== -1) {
167
+ const next = args[cwdIndex + 1];
168
+ if (!next || next.startsWith('--')) {
169
+ console.error('Error: --cwd requires a path argument.');
170
+ process.exit(1);
171
+ }
172
+ cwd = path.resolve(next);
173
+ }
174
+ return {
175
+ help: args.includes('--help'),
176
+ dryRun: args.includes('--dry-run'),
177
+ autoYes: args.includes('--yes'),
178
+ cwd,
179
+ };
180
+ }
181
+
182
+ async function prompt (question) {
183
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
184
+ return new Promise(resolve => {
185
+ rl.question(question, answer => {
186
+ rl.close();
187
+ resolve(answer.trim().toLowerCase());
188
+ });
189
+ });
190
+ }
191
+
192
+ // eslint-disable-next-line complexity
193
+ async function main () {
194
+ const opts = parseArgs(process.argv.slice(2));
195
+ if (opts.help) {
196
+ printHelp();
197
+ process.exit(0);
198
+ }
199
+
200
+ console.log(`\nScanning ${opts.cwd} for deprecated border-radius utility classes...\n`);
201
+
202
+ const extensions = ['.vue', '.md', '.html', '.js', '.ts', '.jsx', '.tsx'];
203
+ const ignore = ['node_modules', 'dist', '.git', '.vuepress/public', '.vuepress/.temp', '.vuepress/.cache', 'storybook-static'];
204
+ const files = await findFiles(opts.cwd, extensions, ignore);
205
+
206
+ const changes = [];
207
+
208
+ for (const file of files) {
209
+ try {
210
+ const content = await fs.readFile(file, 'utf8');
211
+ const { transformed, count } = transformContent(content);
212
+ if (count > 0) {
213
+ changes.push({ file, content, transformed, count });
214
+ }
215
+ } catch (err) {
216
+ console.warn(` ⚠ skipped (read error): ${path.relative(opts.cwd, file)} — ${err.message}`);
217
+ }
218
+ }
219
+
220
+ if (changes.length === 0) {
221
+ console.log('No deprecated border-radius utility classes found. Nothing to migrate.');
222
+ process.exit(0);
223
+ }
224
+
225
+ console.log(`Found ${changes.reduce((sum, c) => sum + c.count, 0)} deprecated border-radius class references across ${changes.length} file(s):\n`);
226
+
227
+ for (const { file, count } of changes) {
228
+ const rel = path.relative(opts.cwd, file);
229
+ console.log(` ${rel} (${count} change${count > 1 ? 's' : ''})`);
230
+ }
231
+
232
+ if (opts.dryRun) {
233
+ console.log('\n--dry-run: No files were modified.\n');
234
+ process.exit(0);
235
+ }
236
+
237
+ if (!opts.autoYes) {
238
+ const answer = await prompt('\nApply changes? (y/N) ');
239
+ if (answer !== 'y' && answer !== 'yes') {
240
+ console.log('Cancelled.');
241
+ process.exit(0);
242
+ }
243
+ }
244
+
245
+ let written = 0;
246
+ let migratedRefs = 0;
247
+ for (const { file, transformed, count } of changes) {
248
+ try {
249
+ await fs.writeFile(file, transformed, 'utf8');
250
+ written++;
251
+ migratedRefs += count;
252
+ } catch (err) {
253
+ console.warn(` ⚠ skipped (write error): ${path.relative(opts.cwd, file)} — ${err.message}`);
254
+ }
255
+ }
256
+
257
+ console.log(`\nMigrated ${migratedRefs} references across ${written} file(s).\n`);
258
+ }
259
+
260
+ const isDirectRun = (() => {
261
+ try {
262
+ return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
263
+ } catch {
264
+ return false;
265
+ }
266
+ })();
267
+
268
+ if (isDirectRun) {
269
+ main().catch(err => {
270
+ console.error(err);
271
+ process.exit(1);
272
+ });
273
+ }
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @fileoverview Tests for dialtone-migrate-border-radius codemod.
5
+ * Run: node packages/dialtone-css/lib/build/js/dialtone_migrate_border_radius/test.mjs
6
+ */
7
+
8
+ import assert from 'node:assert/strict';
9
+ import { createRequire } from 'node:module';
10
+ import { describe, it } from 'node:test';
11
+ import { transformContent, RADIUS_STOP_MAP, PAIR_PREFIX_MAP } from './index.mjs';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // All-corners numeric (d-barN → d-bar-STOP)
15
+ // ---------------------------------------------------------------------------
16
+
17
+ describe('All-corners numeric renames', () => {
18
+ it('d-bar0 → d-bar-0', () => {
19
+ const { transformed, count } = transformContent('<div class="d-bar0" />');
20
+ assert.equal(transformed, '<div class="d-bar-0" />');
21
+ assert.equal(count, 1);
22
+ });
23
+
24
+ it('d-bar1 → d-bar-100', () => {
25
+ const { transformed } = transformContent('<div class="d-bar1" />');
26
+ assert.equal(transformed, '<div class="d-bar-100" />');
27
+ });
28
+
29
+ it('d-bar2 → d-bar-200', () => {
30
+ const { transformed } = transformContent('<div class="d-bar2" />');
31
+ assert.equal(transformed, '<div class="d-bar-200" />');
32
+ });
33
+
34
+ it('d-bar4 → d-bar-300', () => {
35
+ const { transformed } = transformContent('<div class="d-bar4" />');
36
+ assert.equal(transformed, '<div class="d-bar-300" />');
37
+ });
38
+
39
+ it('d-bar6 → d-bar-350', () => {
40
+ const { transformed } = transformContent('<div class="d-bar6" />');
41
+ assert.equal(transformed, '<div class="d-bar-350" />');
42
+ });
43
+
44
+ it('d-bar8 → d-bar-400', () => {
45
+ const { transformed } = transformContent('<div class="d-bar8" />');
46
+ assert.equal(transformed, '<div class="d-bar-400" />');
47
+ });
48
+
49
+ it('d-bar12 → d-bar-450', () => {
50
+ const { transformed } = transformContent('<div class="d-bar12" />');
51
+ assert.equal(transformed, '<div class="d-bar-450" />');
52
+ });
53
+
54
+ it('d-bar16 → d-bar-500', () => {
55
+ const { transformed } = transformContent('<div class="d-bar16" />');
56
+ assert.equal(transformed, '<div class="d-bar-500" />');
57
+ });
58
+
59
+ it('d-bar24 → d-bar-550', () => {
60
+ const { transformed } = transformContent('<div class="d-bar24" />');
61
+ assert.equal(transformed, '<div class="d-bar-550" />');
62
+ });
63
+
64
+ it('d-bar32 → d-bar-600', () => {
65
+ const { transformed } = transformContent('<div class="d-bar32" />');
66
+ assert.equal(transformed, '<div class="d-bar-600" />');
67
+ });
68
+ });
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Pair numeric (d-{btr|bbr|blr|brr}N → d-{bbsr|bber|bisr|bier}-STOP)
72
+ // ---------------------------------------------------------------------------
73
+
74
+ describe('Pair numeric renames', () => {
75
+ it('d-btr6 → d-bbsr-350 (top → block-start)', () => {
76
+ const { transformed } = transformContent('<div class="d-btr6" />');
77
+ assert.equal(transformed, '<div class="d-bbsr-350" />');
78
+ });
79
+
80
+ it('d-btr8 → d-bbsr-400', () => {
81
+ const { transformed } = transformContent('<div class="d-btr8" />');
82
+ assert.equal(transformed, '<div class="d-bbsr-400" />');
83
+ });
84
+
85
+ it('d-bbr8 → d-bber-400 (bottom → block-end)', () => {
86
+ const { transformed } = transformContent('<div class="d-bbr8" />');
87
+ assert.equal(transformed, '<div class="d-bber-400" />');
88
+ });
89
+
90
+ it('d-blr12 → d-bisr-450 (left → inline-start)', () => {
91
+ const { transformed } = transformContent('<div class="d-blr12" />');
92
+ assert.equal(transformed, '<div class="d-bisr-450" />');
93
+ });
94
+
95
+ it('d-brr16 → d-bier-500 (right → inline-end)', () => {
96
+ const { transformed } = transformContent('<div class="d-brr16" />');
97
+ assert.equal(transformed, '<div class="d-bier-500" />');
98
+ });
99
+ });
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Pair keyword (d-{btr|bbr|blr|brr}-{pill|circle} → logical)
103
+ // ---------------------------------------------------------------------------
104
+
105
+ describe('Pair keyword renames', () => {
106
+ it('d-btr-pill → d-bbsr-pill', () => {
107
+ const { transformed } = transformContent('<div class="d-btr-pill" />');
108
+ assert.equal(transformed, '<div class="d-bbsr-pill" />');
109
+ });
110
+
111
+ it('d-btr-circle → d-bbsr-circle', () => {
112
+ const { transformed } = transformContent('<div class="d-btr-circle" />');
113
+ assert.equal(transformed, '<div class="d-bbsr-circle" />');
114
+ });
115
+
116
+ it('d-bbr-pill → d-bber-pill', () => {
117
+ const { transformed } = transformContent('<div class="d-bbr-pill" />');
118
+ assert.equal(transformed, '<div class="d-bber-pill" />');
119
+ });
120
+
121
+ it('d-bbr-circle → d-bber-circle', () => {
122
+ const { transformed } = transformContent('<div class="d-bbr-circle" />');
123
+ assert.equal(transformed, '<div class="d-bber-circle" />');
124
+ });
125
+
126
+ it('d-blr-pill → d-bisr-pill', () => {
127
+ const { transformed } = transformContent('<div class="d-blr-pill" />');
128
+ assert.equal(transformed, '<div class="d-bisr-pill" />');
129
+ });
130
+
131
+ it('d-blr-circle → d-bisr-circle', () => {
132
+ const { transformed } = transformContent('<div class="d-blr-circle" />');
133
+ assert.equal(transformed, '<div class="d-bisr-circle" />');
134
+ });
135
+
136
+ it('d-brr-pill → d-bier-pill', () => {
137
+ const { transformed } = transformContent('<div class="d-brr-pill" />');
138
+ assert.equal(transformed, '<div class="d-bier-pill" />');
139
+ });
140
+
141
+ it('d-brr-circle → d-bier-circle', () => {
142
+ const { transformed } = transformContent('<div class="d-brr-circle" />');
143
+ assert.equal(transformed, '<div class="d-bier-circle" />');
144
+ });
145
+ });
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Multiple classes in one attribute
149
+ // ---------------------------------------------------------------------------
150
+
151
+ describe('Multiple classes in one attribute', () => {
152
+ it('rewrites multiple legacy classes in one pass', () => {
153
+ const { transformed, count } = transformContent('<div class="d-bar6 d-btr8 d-blr-pill" />');
154
+ assert.equal(transformed, '<div class="d-bar-350 d-bbsr-400 d-bisr-pill" />');
155
+ assert.equal(count, 3);
156
+ });
157
+
158
+ it('leaves non-radius classes untouched', () => {
159
+ const { transformed, count } = transformContent('<div class="d-p-200 d-bar6 d-fc-primary" />');
160
+ assert.equal(transformed, '<div class="d-p-200 d-bar-350 d-fc-primary" />');
161
+ assert.equal(count, 1);
162
+ });
163
+ });
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Already-migrated classes — no changes
167
+ // ---------------------------------------------------------------------------
168
+
169
+ describe('Does NOT transform already-migrated classes', () => {
170
+ it('ignores d-bar-350 (already new format)', () => {
171
+ const { transformed, count } = transformContent('<div class="d-bar-350" />');
172
+ assert.equal(transformed, '<div class="d-bar-350" />');
173
+ assert.equal(count, 0);
174
+ });
175
+
176
+ it('ignores d-bbsr-400', () => {
177
+ const { transformed, count } = transformContent('<div class="d-bbsr-400" />');
178
+ assert.equal(transformed, '<div class="d-bbsr-400" />');
179
+ assert.equal(count, 0);
180
+ });
181
+
182
+ it('ignores d-bber-pill', () => {
183
+ const { transformed, count } = transformContent('<div class="d-bber-pill" />');
184
+ assert.equal(transformed, '<div class="d-bber-pill" />');
185
+ assert.equal(count, 0);
186
+ });
187
+
188
+ it('ignores d-bisr-circle', () => {
189
+ const { transformed, count } = transformContent('<div class="d-bisr-circle" />');
190
+ assert.equal(transformed, '<div class="d-bisr-circle" />');
191
+ assert.equal(count, 0);
192
+ });
193
+
194
+ it('ignores d-bar-pill (all-corners keyword, not deprecated)', () => {
195
+ const { transformed, count } = transformContent('<div class="d-bar-pill" />');
196
+ assert.equal(transformed, '<div class="d-bar-pill" />');
197
+ assert.equal(count, 0);
198
+ });
199
+
200
+ it('ignores d-bar-circle (all-corners keyword, not deprecated)', () => {
201
+ const { transformed, count } = transformContent('<div class="d-bar-circle" />');
202
+ assert.equal(transformed, '<div class="d-bar-circle" />');
203
+ assert.equal(count, 0);
204
+ });
205
+ });
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Does NOT match inside unrelated class names
209
+ // ---------------------------------------------------------------------------
210
+
211
+ describe('Does NOT match substrings inside other classes', () => {
212
+ it('ignores foo-d-bar6', () => {
213
+ const { transformed, count } = transformContent('<div class="foo-d-bar6" />');
214
+ assert.equal(transformed, '<div class="foo-d-bar6" />');
215
+ assert.equal(count, 0);
216
+ });
217
+
218
+ it('ignores my-d-btr8', () => {
219
+ const { transformed, count } = transformContent('<div class="my-d-btr8" />');
220
+ assert.equal(transformed, '<div class="my-d-btr8" />');
221
+ assert.equal(count, 0);
222
+ });
223
+
224
+ it('ignores app-d-brr-pill', () => {
225
+ const { transformed, count } = transformContent('<div class="app-d-brr-pill" />');
226
+ assert.equal(transformed, '<div class="app-d-brr-pill" />');
227
+ assert.equal(count, 0);
228
+ });
229
+ });
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // Unrelated classes — no false positives
233
+ // ---------------------------------------------------------------------------
234
+
235
+ describe('Does NOT touch unrelated utilities', () => {
236
+ it('ignores d-p-200 d-m-100 d-fc-primary', () => {
237
+ const input = '<div class="d-p-200 d-m-100 d-fc-primary" />';
238
+ const { transformed, count } = transformContent(input);
239
+ assert.equal(transformed, input);
240
+ assert.equal(count, 0);
241
+ });
242
+
243
+ it('ignores d-ba d-baw2 d-bas-dashed', () => {
244
+ const input = '<div class="d-ba d-baw2 d-bas-dashed" />';
245
+ const { transformed, count } = transformContent(input);
246
+ assert.equal(transformed, input);
247
+ assert.equal(count, 0);
248
+ });
249
+ });
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Different quote styles
253
+ // ---------------------------------------------------------------------------
254
+
255
+ describe('Different quote styles and contexts', () => {
256
+ it('handles single-quoted class attribute', () => {
257
+ const { transformed } = transformContent('<div class=\'d-btr8\' />');
258
+ assert.equal(transformed, '<div class=\'d-bbsr-400\' />');
259
+ });
260
+
261
+ it('handles class in a Vue template', () => {
262
+ const input = '<template><div class="d-bar6 d-brr-circle" /></template>';
263
+ const expected = '<template><div class="d-bar-350 d-bier-circle" /></template>';
264
+ const { transformed } = transformContent(input);
265
+ assert.equal(transformed, expected);
266
+ });
267
+ });
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // Template literals (JS/TS backtick strings)
271
+ // ---------------------------------------------------------------------------
272
+
273
+ describe('Template literal delimiters', () => {
274
+ it('transforms d-bar6 inside a simple template literal', () => {
275
+ const input = 'const cls = `d-bar6`;';
276
+ const expected = 'const cls = `d-bar-350`;';
277
+ const { transformed, count } = transformContent(input);
278
+ assert.equal(transformed, expected);
279
+ assert.equal(count, 1);
280
+ });
281
+
282
+ it('transforms pair keyword inside a template literal', () => {
283
+ const input = 'const cls = `d-btr-pill`;';
284
+ const expected = 'const cls = `d-bbsr-pill`;';
285
+ const { transformed } = transformContent(input);
286
+ assert.equal(transformed, expected);
287
+ });
288
+
289
+ it('transforms pair numeric inside a template literal', () => {
290
+ const input = 'const cls = `d-brr16`;';
291
+ const expected = 'const cls = `d-bier-500`;';
292
+ const { transformed } = transformContent(input);
293
+ assert.equal(transformed, expected);
294
+ });
295
+
296
+ it('transforms multiple classes inside a template literal', () => {
297
+ const input = 'const cls = `d-bar8 d-btr-circle`;';
298
+ const expected = 'const cls = `d-bar-400 d-bbsr-circle`;';
299
+ const { transformed, count } = transformContent(input);
300
+ assert.equal(transformed, expected);
301
+ assert.equal(count, 2);
302
+ });
303
+
304
+ it('transforms class inside a template literal with interpolation', () => {
305
+ const input = 'const cls = `d-bar6 ${otherClass}`;';
306
+ const expected = 'const cls = `d-bar-350 ${otherClass}`;';
307
+ const { transformed, count } = transformContent(input);
308
+ assert.equal(transformed, expected);
309
+ assert.equal(count, 1);
310
+ });
311
+
312
+ it('does not match inside a non-delimited context (no false positive)', () => {
313
+ const input = 'const cls = `foo-d-bar6`;';
314
+ const { transformed, count } = transformContent(input);
315
+ assert.equal(transformed, input);
316
+ assert.equal(count, 0);
317
+ });
318
+ });
319
+
320
+ // ---------------------------------------------------------------------------
321
+ // Edge cases
322
+ // ---------------------------------------------------------------------------
323
+
324
+ describe('Edge cases', () => {
325
+ it('returns count of 0 for content with no matches', () => {
326
+ const { count } = transformContent('<div><span>No radius here</span></div>');
327
+ assert.equal(count, 0);
328
+ });
329
+
330
+ it('handles empty string', () => {
331
+ const { transformed, count } = transformContent('');
332
+ assert.equal(transformed, '');
333
+ assert.equal(count, 0);
334
+ });
335
+
336
+ it('handles d-bar32 at end of attribute (before closing quote)', () => {
337
+ const { transformed } = transformContent('<div class="d-p-200 d-bar32" />');
338
+ assert.equal(transformed, '<div class="d-p-200 d-bar-600" />');
339
+ });
340
+
341
+ it('handles class at start of attribute (after opening quote)', () => {
342
+ const { transformed } = transformContent('<div class="d-bar6 d-p-200" />');
343
+ assert.equal(transformed, '<div class="d-bar-350 d-p-200" />');
344
+ });
345
+ });
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // Real-world patterns
349
+ // ---------------------------------------------------------------------------
350
+
351
+ describe('Real-world patterns', () => {
352
+ it('transforms radius in a card-like component', () => {
353
+ const input = '<div class="d-p-200 d-bgc-primary d-bar8 d-bs-sm">';
354
+ const expected = '<div class="d-p-200 d-bgc-primary d-bar-400 d-bs-sm">';
355
+ const { transformed } = transformContent(input);
356
+ assert.equal(transformed, expected);
357
+ });
358
+
359
+ it('transforms top-only radius on a header', () => {
360
+ const input = '<div class="d-btr6 d-bgc-moderate">';
361
+ const expected = '<div class="d-bbsr-350 d-bgc-moderate">';
362
+ const { transformed } = transformContent(input);
363
+ assert.equal(transformed, expected);
364
+ });
365
+
366
+ it('transforms bottom pill radius', () => {
367
+ const input = '<div class="d-bbr-pill d-bgc-info">';
368
+ const expected = '<div class="d-bber-pill d-bgc-info">';
369
+ const { transformed } = transformContent(input);
370
+ assert.equal(transformed, expected);
371
+ });
372
+
373
+ it('transforms mixed numeric and keyword across multiple classes', () => {
374
+ const input = '<div class="d-bar6 d-btr-pill d-bbr-circle d-blr8 d-brr-pill">';
375
+ const expected = '<div class="d-bar-350 d-bbsr-pill d-bber-circle d-bisr-400 d-bier-pill">';
376
+ const { transformed, count } = transformContent(input);
377
+ assert.equal(transformed, expected);
378
+ assert.equal(count, 5);
379
+ });
380
+ });
381
+
382
+ // ---------------------------------------------------------------------------
383
+ // Contract: parity with ESLint rule maps
384
+ // ---------------------------------------------------------------------------
385
+
386
+ describe('Contract: maps match ESLint rule', () => {
387
+ const require = createRequire(import.meta.url);
388
+
389
+ let eslintSource;
390
+ try {
391
+ const fs = require('fs');
392
+ const path = require('path');
393
+ eslintSource = fs.readFileSync(
394
+ path.resolve(
395
+ import.meta.dirname,
396
+ '../../../../../eslint-plugin-dialtone/lib/rules/deprecated-radius-utility-classes.js',
397
+ ),
398
+ 'utf8',
399
+ );
400
+ } catch { /* will fail tests below with clear message */ }
401
+
402
+ function extractObject (source, varName) {
403
+ const re = new RegExp(`const ${varName}\\s*=\\s*\\{([^}]+)\\}`);
404
+ const match = source?.match(re);
405
+ if (!match) return null;
406
+ return Function(`return {${match[1]}}`)();
407
+ }
408
+
409
+ it('RADIUS_STOP_MAP matches ESLint rule', () => {
410
+ assert.ok(eslintSource, 'Could not read ESLint rule source file');
411
+ const eslintMap = extractObject(eslintSource, 'RADIUS_STOP_MAP');
412
+ assert.ok(eslintMap, 'Could not parse RADIUS_STOP_MAP from ESLint rule');
413
+ assert.deepEqual(RADIUS_STOP_MAP, eslintMap);
414
+ });
415
+
416
+ it('PAIR_PREFIX_MAP matches ESLint rule', () => {
417
+ assert.ok(eslintSource, 'Could not read ESLint rule source file');
418
+ const eslintMap = extractObject(eslintSource, 'PAIR_PREFIX_MAP');
419
+ assert.ok(eslintMap, 'Could not parse PAIR_PREFIX_MAP from ESLint rule');
420
+ assert.deepEqual(PAIR_PREFIX_MAP, eslintMap);
421
+ });
422
+ });