Skip to content

Commit 6bc3599

Browse files
feat(css-utilities, eslint-plugin-dialtone): DLT-3329 update border-radius css utilities and associated tooling (#1211)
1 parent 0278620 commit 6bc3599

18 files changed

Lines changed: 826 additions & 180 deletions

packages/dialtone-css/lib/build/js/dialtone_migration_helper/configs/utility-class-to-token-stops.mjs

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,35 @@ const SPACING_LAYOUT_MAP = {
4141
96: '150', 128: '200',
4242
};
4343

44-
// Helper: build regex that matches class names with word boundaries
45-
// Sorted by descending key length to avoid partial matches (d-h1024 before d-h102)
44+
// Border-radius: legacy pixel value → new radius token stop
45+
// MUST STAY IN SYNC with RADIUS_STOPS in dialtone-css/postcss/constants.cjs.
46+
const RADIUS_MAP = {
47+
0: '0', 1: '100', 2: '200', 4: '300', 6: '350',
48+
8: '400', 12: '450', 16: '500', 24: '550', 32: '600',
49+
};
50+
51+
// Border-radius: legacy physical-side prefix → new logical prefix.
52+
const RADIUS_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+
// Class-name boundary: preceded by space, quote, or start; followed by space, quote, or end.
60+
const CLASS_BOUNDARY_LEFT = `((?:^|["'\\s]))`;
61+
const CLASS_BOUNDARY_RIGHT = `((?:["'\\s]|$))`;
62+
63+
// Build regex that matches class names ending in any key from `map`, with boundaries.
64+
// Keys sorted by descending length to avoid partial matches (d-h1024 before d-h102).
4665
function buildClassRegex (prefix, map) {
4766
const keys = Object.keys(map).sort((a, b) => b.length - a.length || Number(b) - Number(a));
48-
const pattern = keys.join('|');
49-
// Match class name boundary: preceded by space, quote, or start; followed by space, quote, or end
50-
return new RegExp(`((?:^|["'\\s]))${prefix}(${pattern})((?:["'\\s]|$))`, 'gm');
67+
return new RegExp(`${CLASS_BOUNDARY_LEFT}${prefix}(${keys.join('|')})${CLASS_BOUNDARY_RIGHT}`, 'gm');
68+
}
69+
70+
// Variant for fixed-keyword suffixes (e.g. `-pill`, `-circle`).
71+
function buildKeywordClassRegex (prefix, keyword) {
72+
return new RegExp(`${CLASS_BOUNDARY_LEFT}${prefix}-${keyword}${CLASS_BOUNDARY_RIGHT}`, 'gm');
5173
}
5274

5375
export default {
@@ -59,10 +81,42 @@ export default {
5981
'- Padding: d-p8 → d-p-100, d-pt16 → d-pt-200\n' +
6082
'- Gap: d-g8 → d-g-100, d-rg16 → d-rg-200\n' +
6183
'- Position: d-t8 → d-t-100, d-tn8 → d-t-n100\n' +
84+
'- Border-radius all: d-bar6 → d-bar-350, d-bar24 → d-bar-550\n' +
85+
'- Border-radius pair (physical → logical): d-btr6 → d-bbsr-350, d-bbr8 → d-bber-400, d-blr12 → d-bisr-450, d-brr16 → d-bier-500\n' +
86+
'- Border-radius pair keyword: d-btr-pill → d-bbsr-pill, d-brr-circle → d-bier-circle\n' +
6287
'- Old deprecated sizes (d-h72, d-w332, etc.) are left unchanged for manual review.\n',
63-
patterns: ['**/*.{vue,html,js,ts,jsx,tsx,md,less,css}'],
88+
patterns: ['**/*.{vue,html,js,ts,jsx,tsx,md,mdx,less,css}'],
6489
globbyConfig: {
65-
ignore: ['**/dialtone_migration_helper/tests/**', '**/node_modules/**'],
90+
// Include dotfiles/dotdirs so tooling directories like `.vuepress/baseComponents/`,
91+
// `.storybook/`, and per-repo docs folders are scanned. Dotted build-output caches are
92+
// explicitly excluded below.
93+
dot: true,
94+
ignore: [
95+
'**/node_modules/**',
96+
// `dot: true` makes dot-dirs globbable; explicitly exclude the git directory.
97+
'**/.git/**',
98+
// Built outputs: regenerated on next build; rewriting selectors in co-selected rules
99+
// (`.d-bar-350, .d-bar6 { ... }`) would corrupt them since the leading whitespace
100+
// before the legacy selector looks like a class boundary to the regex.
101+
'**/dist/**',
102+
'**/build/**',
103+
'**/lib/dist/**',
104+
// Framework caches
105+
'**/.cache/**',
106+
'**/.vite/**',
107+
'**/.vuepress/.cache/**',
108+
'**/.vuepress/.temp/**',
109+
'**/.vuepress/dist/**',
110+
'**/.next/**',
111+
'**/.nuxt/**',
112+
'**/.turbo/**',
113+
'**/.nx/**',
114+
// Migration-helper test fixtures intentionally contain legacy class names.
115+
'**/dialtone_migration_helper/tests/**',
116+
// ESLint-plugin rules and tests inherently contain legacy class names as regex patterns
117+
// and test inputs — they're the tool that detects the legacy classes, don't rewrite them.
118+
'**/eslint-plugin-dialtone/**',
119+
],
66120
},
67121
expressions: [
68122
// ── Sizing: d-h{px} → d-h-{layout-stop} ──────────────────────────────
@@ -135,5 +189,27 @@ export default {
135189
from: buildClassRegex(`d-${prefix}n`, NEGATIVE_SPACING_MAP),
136190
to: (match, pre, px, post) => `${pre}d-${prefix}-n${NEGATIVE_SPACING_MAP[px]}${post}`,
137191
})),
192+
193+
// ── Border-radius all-corners numeric: d-bar{px} → d-bar-{stop} ──────
194+
{
195+
from: buildClassRegex('d-bar', RADIUS_MAP),
196+
to: (match, pre, px, post) => `${pre}d-bar-${RADIUS_MAP[px]}${post}`,
197+
},
198+
199+
// ── Border-radius side-pair numeric: d-{legacy}{px} → d-{logical}-{stop}
200+
// Physical pair prefixes (btr/bbr/blr/brr) rewrite to their logical siblings (bbsr/bber/bisr/bier).
201+
...Object.entries(RADIUS_PAIR_PREFIX_MAP).map(([legacy, logical]) => ({
202+
from: buildClassRegex(`d-${legacy}`, RADIUS_MAP),
203+
to: (match, pre, px, post) => `${pre}d-${logical}-${RADIUS_MAP[px]}${post}`,
204+
})),
205+
206+
// ── Border-radius side-pair keyword: d-{legacy}-{pill|circle} → d-{logical}-{pill|circle}
207+
// Legacy `.d-bar-pill` / `.d-bar-circle` stay as-is (same name in the new scheme).
208+
...Object.entries(RADIUS_PAIR_PREFIX_MAP).flatMap(([legacy, logical]) =>
209+
['pill', 'circle'].map(keyword => ({
210+
from: buildKeywordClassRegex(`d-${legacy}`, keyword),
211+
to: (match, pre, post) => `${pre}d-${logical}-${keyword}${post}`,
212+
})),
213+
),
138214
],
139215
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<!--
2+
Manual-test fixture for the radius portion of the `utility-class-to-token-stops` migration.
3+
Point `pnpm dialtone-migration-helper` at this file to exercise every rewrite in one pass.
4+
5+
Expected transformations:
6+
- d-bar{N} → d-bar-{stop}
7+
- d-btr{N} → d-bbsr-{stop}
8+
- d-bbr{N} → d-bber-{stop}
9+
- d-blr{N} → d-bisr-{stop}
10+
- d-brr{N} → d-bier-{stop}
11+
- d-{btr|bbr|blr|brr}-{pill|circle} → d-{bbsr|bber|bisr|bier}-{pill|circle}
12+
- d-bar-pill, d-bar-circle, d-bar-unset: unchanged (canonical)
13+
-->
14+
<template>
15+
<!-- All-corners: every legacy numeric stop -->
16+
<div class="d-bar0">bar0 → bar-0</div>
17+
<div class="d-bar1">bar1 → bar-100</div>
18+
<div class="d-bar2">bar2 → bar-200</div>
19+
<div class="d-bar4">bar4 → bar-300</div>
20+
<div class="d-bar6">bar6 → bar-350</div>
21+
<div class="d-bar8">bar8 → bar-400</div>
22+
<div class="d-bar12">bar12 → bar-450</div>
23+
<div class="d-bar16">bar16 → bar-500</div>
24+
<div class="d-bar24">bar24 → bar-550 (new token)</div>
25+
<div class="d-bar32">bar32 → bar-600</div>
26+
27+
<!-- Side-pair numeric: physical → logical -->
28+
<div class="d-btr6">btr6 → bbsr-350 (top pair)</div>
29+
<div class="d-bbr8">bbr8 → bber-400 (bottom pair)</div>
30+
<div class="d-blr12">blr12 → bisr-450 (left pair)</div>
31+
<div class="d-brr16">brr16 → bier-500 (right pair)</div>
32+
<div class="d-btr24">btr24 → bbsr-550</div>
33+
<div class="d-bbr32">bbr32 → bber-600</div>
34+
35+
<!-- Side-pair keyword: physical → logical -->
36+
<div class="d-btr-pill">btr-pill → bbsr-pill</div>
37+
<div class="d-btr-circle">btr-circle → bbsr-circle</div>
38+
<div class="d-bbr-pill">bbr-pill → bber-pill</div>
39+
<div class="d-blr-pill">blr-pill → bisr-pill</div>
40+
<div class="d-brr-circle">brr-circle → bier-circle</div>
41+
42+
<!-- Canonical names: unchanged -->
43+
<div class="d-bar-pill">bar-pill (unchanged)</div>
44+
<div class="d-bar-circle">bar-circle (unchanged)</div>
45+
<div class="d-bar-unset">bar-unset (unchanged)</div>
46+
47+
<!-- Mixed in one class attribute (multi-class rewrite) -->
48+
<div class="d-p-200 d-bar6 d-btr8 d-blr-pill d-fc-primary">
49+
mixed: bar6+btr8+blr-pill get rewritten; d-p-200 and d-fc-primary untouched
50+
</div>
51+
52+
<!-- Class binding via object syntax (plain strings inside) -->
53+
<div :class="{ 'd-bar6': rounded, 'd-btr-pill': pillTop }">
54+
object-syntax binding
55+
</div>
56+
57+
<!-- Class binding via template literal -->
58+
<div :class="`d-bar${size} d-btr${size}`">
59+
template-literal (only matches literal suffixes; dynamic interpolation stays as-is)
60+
</div>
61+
62+
<!-- New logical names (already correct): not touched -->
63+
<div class="d-bar-350 d-bbsr-400 d-bier-pill d-bssr-500 d-beer-circle">
64+
new logical names stay as-is
65+
</div>
66+
</template>

packages/dialtone-css/lib/build/js/dialtone_migration_helper/tests/utility-class-to-token-stops.test.mjs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,83 @@ describe('utility-class-to-token-stops config', () => {
8888
assert.equal(apply(input), expected);
8989
});
9090
});
91+
92+
// ─── Border-radius migration (DLT-3329) ───────────────────────────────
93+
94+
describe('border-radius all-corners — legacy pixel-suffix to token stop', () => {
95+
const cases = [
96+
['d-bar0', 'd-bar-0'],
97+
['d-bar1', 'd-bar-100'],
98+
['d-bar2', 'd-bar-200'],
99+
['d-bar4', 'd-bar-300'],
100+
['d-bar6', 'd-bar-350'],
101+
['d-bar8', 'd-bar-400'],
102+
['d-bar12', 'd-bar-450'],
103+
['d-bar16', 'd-bar-500'],
104+
['d-bar24', 'd-bar-550'],
105+
['d-bar32', 'd-bar-600'],
106+
];
107+
for (const [from, to] of cases) {
108+
it(`${from}${to}`, () => {
109+
assert.equal(apply(`<div class="${from}" />`), `<div class="${to}" />`);
110+
});
111+
}
112+
});
113+
114+
describe('border-radius side-pair numeric — physical prefix to logical prefix', () => {
115+
const cases = [
116+
['d-btr6', 'd-bbsr-350'], // top → block-start pair
117+
['d-bbr8', 'd-bber-400'], // bottom → block-end pair
118+
['d-blr12', 'd-bisr-450'], // left → inline-start pair
119+
['d-brr16', 'd-bier-500'], // right → inline-end pair
120+
['d-btr24', 'd-bbsr-550'], // new 550 stop
121+
['d-bbr32', 'd-bber-600'],
122+
['d-blr0', 'd-bisr-0'],
123+
['d-brr1', 'd-bier-100'],
124+
];
125+
for (const [from, to] of cases) {
126+
it(`${from}${to}`, () => {
127+
assert.equal(apply(`<div class="${from}" />`), `<div class="${to}" />`);
128+
});
129+
}
130+
});
131+
132+
describe('border-radius side-pair keyword — pill/circle', () => {
133+
const cases = [
134+
['d-btr-pill', 'd-bbsr-pill'],
135+
['d-btr-circle', 'd-bbsr-circle'],
136+
['d-bbr-pill', 'd-bber-pill'],
137+
['d-bbr-circle', 'd-bber-circle'],
138+
['d-blr-pill', 'd-bisr-pill'],
139+
['d-blr-circle', 'd-bisr-circle'],
140+
['d-brr-pill', 'd-bier-pill'],
141+
['d-brr-circle', 'd-bier-circle'],
142+
];
143+
for (const [from, to] of cases) {
144+
it(`${from}${to}`, () => {
145+
assert.equal(apply(`<div class="${from}" />`), `<div class="${to}" />`);
146+
});
147+
}
148+
});
149+
150+
describe('border-radius canonical keyword names — unchanged', () => {
151+
const cases = [
152+
'd-bar-pill',
153+
'd-bar-circle',
154+
'd-bar-unset',
155+
];
156+
for (const unchanged of cases) {
157+
it(`${unchanged} stays as ${unchanged}`, () => {
158+
assert.equal(apply(`<div class="${unchanged}" />`), `<div class="${unchanged}" />`);
159+
});
160+
}
161+
});
162+
163+
describe('border-radius multi-class migration', () => {
164+
it('rewrites mixed legacy classes in one string', () => {
165+
const input = `<div class="d-bar6 d-btr8 d-blr-pill d-p-200" />`;
166+
const expected = `<div class="d-bar-350 d-bbsr-400 d-bisr-pill d-p-200" />`;
167+
assert.equal(apply(input), expected);
168+
});
169+
});
91170
});

packages/dialtone-css/lib/build/less/components/box.less

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@
185185
.d-box--bc-positive-subtle { --box-bc: var(--dt-color-border-success-subtle); }
186186
.d-box--bc-positive-strong { --box-bc: var(--dt-color-border-success-strong); }
187187

188-
@box-border-radius-values: 0, 100, 200, 300, 350, 400, 450, 500, 600;
188+
@box-border-radius-values: 0, 100, 200, 300, 350, 400, 450, 500, 550, 600;
189189

190190
// Border width — 7 axes × border-width scale
191191
.d-box--bw { ._box-border-width(--box-bw); }

packages/dialtone-css/lib/build/less/utilities/borders.less

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -117,59 +117,7 @@
117117
// ============================================================================
118118
// $ BORDER RADIUS
119119
// ============================================================================
120-
// $$ ALL
121-
// ----------------------------------------------------------------------------
122-
.d-bar-circle { border-radius: var(--dt-size-radius-circle) !important; }
123-
.d-bar-pill { border-radius: var(--dt-size-radius-pill) !important; }
124-
.d-bar-unset { border-radius: unset !important; }
125-
126-
// $$ TOP
127-
// ----------------------------------------------------------------------------
128-
.d-btr-circle {
129-
border-start-start-radius: var(--dt-size-radius-circle) !important;
130-
border-start-end-radius: var(--dt-size-radius-circle) !important;
131-
}
132-
133-
.d-btr-pill {
134-
border-start-start-radius: var(--dt-size-radius-pill) !important;
135-
border-start-end-radius: var(--dt-size-radius-pill) !important;
136-
}
137-
138-
// $$ RIGHT
139-
// ----------------------------------------------------------------------------
140-
.d-brr-circle {
141-
border-start-end-radius: var(--dt-size-radius-circle) !important;
142-
border-end-end-radius: var(--dt-size-radius-circle) !important;
143-
}
144-
145-
.d-brr-pill {
146-
border-start-end-radius: var(--dt-size-radius-pill) !important;
147-
border-end-end-radius: var(--dt-size-radius-pill) !important;
148-
}
149-
150-
// $$ BOTTOM
151-
// ----------------------------------------------------------------------------
152-
.d-bbr-circle {
153-
border-end-end-radius: var(--dt-size-radius-circle) !important;
154-
border-end-start-radius: var(--dt-size-radius-circle) !important;
155-
}
156-
157-
.d-bbr-pill {
158-
border-end-end-radius: var(--dt-size-radius-pill) !important;
159-
border-end-start-radius: var(--dt-size-radius-pill) !important;
160-
}
161-
162-
// $$ RIGHT
163-
// ----------------------------------------------------------------------------
164-
.d-blr-circle {
165-
border-start-start-radius: var(--dt-size-radius-circle) !important;
166-
border-end-start-radius: var(--dt-size-radius-circle) !important;
167-
}
168-
169-
.d-blr-pill {
170-
border-start-start-radius: var(--dt-size-radius-pill) !important;
171-
border-end-start-radius: var(--dt-size-radius-pill) !important;
172-
}
120+
// Radius utilities are emitted by borderUtilities() in postcss/dialtone-generators.cjs.
173121

174122

175123
// $$ STYLE

packages/dialtone-css/postcss/constants.cjs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,24 @@ module.exports = {
2222
0,
2323
],
2424
FLEX_COLUMNS: 12,
25-
BORDER_RADIUS_SIZES: {
26-
0: 'radius-0',
27-
1: 'radius-100',
28-
2: 'radius-200',
29-
4: 'radius-300',
30-
6: 'radius-350',
31-
8: 'radius-400',
32-
12: 'radius-450',
33-
16: 'radius-500',
34-
24: '550', // TODO: Remove as it doesn't have a valid token?
35-
32: 'radius-600',
36-
},
25+
// RADIUS_STOPS: drives border-radius utility generation.
26+
// Each entry pairs a logical token stop with its legacy t-shirt pixel suffix
27+
// so the generator can emit co-selected rules like `.d-bar-350, .d-bar6 { ... }`.
28+
// Stops `pill` and `circle` use themselves as the legacy suffix (self-aliased).
29+
RADIUS_STOPS: [
30+
{ stop: '0', legacyPx: '0' },
31+
{ stop: '100', legacyPx: '1' },
32+
{ stop: '200', legacyPx: '2' },
33+
{ stop: '300', legacyPx: '4' },
34+
{ stop: '350', legacyPx: '6' },
35+
{ stop: '400', legacyPx: '8' },
36+
{ stop: '450', legacyPx: '12' },
37+
{ stop: '500', legacyPx: '16' },
38+
{ stop: '550', legacyPx: '24' },
39+
{ stop: '600', legacyPx: '32' },
40+
{ stop: 'pill', legacyPx: 'pill' },
41+
{ stop: 'circle', legacyPx: 'circle' },
42+
],
3743
GAP_SPACES: {
3844
0: '0',
3945
1: '100',

0 commit comments

Comments
 (0)