Skip to content

Commit d94dc4b

Browse files
feat(eslint-plugin-dialtone): DLT-3047 physical-to-logical naming migration tooling (#1163)
1 parent 21a011f commit d94dc4b

6 files changed

Lines changed: 1056 additions & 0 deletions

File tree

.claude/rules/logical-naming.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Logical Naming Convention
22

3+
## Enforcement
4+
5+
**Never introduce physical direction names** (`left`, `right`, `top`, `bottom`, `alpha`, `omega`) in new component slots, props, events, or prop values. Always use logical equivalents (`start`, `end`, `blockStart`, `blockEnd`).
6+
7+
When reviewing code or creating new components:
8+
9+
- New slots must use logical names: e.g. `startIcon`, `endIcon`, `start`, `end`, `blockStart`, `blockEnd`
10+
- New props must use logical names: e.g. `startClass`, `endClass`, `startDisabled`, etc.
11+
- New prop values for positioning must be logical: e.g. `start`, `end`, `blockStart`, `blockEnd`
12+
- New events must use logical names: e.g. `start-clicked`, `end-clicked`
13+
- If touching a component that still has physical-only names, add logical alternatives with deprecated fallbacks.
14+
315
## Direction Names
416

517
Use logical names in all new component code, e.g.:
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
---
2+
heading: 'Migrating to Logical Naming for Slots, Props, and Events'
3+
author: Francis Rupert
4+
posted: '2026-4-6'
5+
excerpt: 'Vue component APIs now use logical direction names (start/end/blockStart/blockEnd) instead of physical names (left/right/top/bottom/alpha/omega). Backward-compatible, with ESLint rule and migration script.'
6+
---
7+
8+
<BlogPost :author="$frontmatter.author" :posted="parse($frontmatter.posted, 'y-M-d', new Date())" :heading="$frontmatter.heading" :excerpt="$frontmatter.excerpt">
9+
10+
## TLDR
11+
12+
- Slots, props, prop values, and events now use **logical direction names**: `start`, `end`, `blockStart`, `blockEnd`.
13+
- Physical names (`left`, `right`, `top`, `bottom`, `alpha`, `omega`) still work -- this is **not a breaking change**.
14+
- Use the [ESLint rule](#eslint-rule) or [migration script](#migration-script) to update your code.
15+
- **Manual migration required**: The `#icon` slot on `dt-button` -- see [below](#manual-migration-dt-button-icon-slot).
16+
17+
## Why Logical?
18+
19+
- **RTL and internationalization.** Logical names respect writing direction -- `start` means the inline-start edge regardless of locale.
20+
- **CSS alignment.** Matches CSS logical properties (`margin-inline-start`, `padding-block-end`) that Dialtone already uses internally.
21+
- **Consistency.** One naming convention across tokens, CSS utilities, and Vue components.
22+
23+
## Examples
24+
25+
### Slots
26+
27+
<div class="d-d-grid d-g16 d-g-cols1 md:d-g-cols2">
28+
<div>
29+
30+
**Before**
31+
32+
```vue
33+
<dt-badge>
34+
<template #leftIcon>...</template>
35+
</dt-badge>
36+
37+
<dt-item-layout>
38+
<template #left>...</template>
39+
<template #right>...</template>
40+
<template #bottom>...</template>
41+
</dt-item-layout>
42+
43+
<dt-split-button>
44+
<template #alphaIcon>...</template>
45+
<template #omegaIcon>...</template>
46+
</dt-split-button>
47+
```
48+
49+
</div>
50+
<div>
51+
52+
**After**
53+
54+
```vue
55+
<dt-badge>
56+
<template #startIcon>...</template>
57+
</dt-badge>
58+
59+
<dt-item-layout>
60+
<template #start>...</template>
61+
<template #end>...</template>
62+
<template #blockEnd>...</template>
63+
</dt-item-layout>
64+
65+
<dt-split-button>
66+
<template #startIcon>...</template>
67+
<template #endIcon>...</template>
68+
</dt-split-button>
69+
```
70+
71+
</div>
72+
</div>
73+
74+
### Props
75+
76+
<div class="d-d-grid d-g16 d-g-cols1 md:d-g-cols2">
77+
<div>
78+
79+
**Before**
80+
81+
```vue
82+
<dt-item-layout
83+
left-class="d-bgc-critical"
84+
right-class="d-bgc-warning"
85+
bottom-class="d-bgc-info"
86+
/>
87+
88+
<dt-split-button
89+
alpha-active
90+
alpha-aria-label="Call"
91+
omega-disabled
92+
/>
93+
```
94+
95+
</div>
96+
<div>
97+
98+
**After**
99+
100+
```vue
101+
<dt-item-layout
102+
start-class="d-bgc-critical"
103+
end-class="d-bgc-warning"
104+
block-end-class="d-bgc-info"
105+
/>
106+
107+
<dt-split-button
108+
start-active
109+
start-aria-label="Call"
110+
end-disabled
111+
/>
112+
```
113+
114+
</div>
115+
</div>
116+
117+
### Prop Values
118+
119+
<div class="d-d-grid d-g16 d-g-cols1 md:d-g-cols2">
120+
<div>
121+
122+
**Before**
123+
124+
```vue
125+
<dt-button icon-position="left">
126+
...
127+
</dt-button>
128+
129+
<dt-root-layout sidebar-position="right" />
130+
```
131+
132+
</div>
133+
<div>
134+
135+
**After**
136+
137+
```vue
138+
<dt-button icon-position="start">
139+
...
140+
</dt-button>
141+
142+
<dt-root-layout sidebar-position="end" />
143+
```
144+
145+
</div>
146+
</div>
147+
148+
### Events
149+
150+
<div class="d-d-grid d-g16 d-g-cols1 md:d-g-cols2">
151+
<div>
152+
153+
**Before**
154+
155+
```vue
156+
<dt-split-button
157+
@alpha-clicked="onPrimary"
158+
@omega-clicked="onSecondary"
159+
/>
160+
```
161+
162+
</div>
163+
<div>
164+
165+
**After**
166+
167+
```vue
168+
<dt-split-button
169+
@start-clicked="onPrimary"
170+
@end-clicked="onSecondary"
171+
/>
172+
```
173+
174+
</div>
175+
</div>
176+
177+
## Manual Migration: dt-button #icon Slot
178+
179+
The `#icon` slot on `dt-button` is **ambiguous** -- its position depends on the `iconPosition` prop. The migration tools skip this case. Replace `#icon` with the slot matching your intended position:
180+
181+
| iconPosition value | Replacement slot |
182+
| --- | --- |
183+
| `start` (default) | `#startIcon` |
184+
| `end` | `#endIcon` |
185+
| `blockStart` | `#blockStartIcon` |
186+
| `blockEnd` | `#blockEndIcon` |
187+
188+
<div class="d-d-grid d-g16 d-g-cols1 md:d-g-cols2">
189+
<div>
190+
191+
**Before**
192+
193+
```vue
194+
<dt-button icon-position="start">
195+
<template #icon>
196+
<dt-icon name="phone" />
197+
</template>
198+
Call
199+
</dt-button>
200+
```
201+
202+
</div>
203+
<div>
204+
205+
**After**
206+
207+
```vue
208+
<dt-button icon-position="start">
209+
<template #startIcon>
210+
<dt-icon name="phone" />
211+
</template>
212+
Call
213+
</dt-button>
214+
```
215+
216+
</div>
217+
</div>
218+
219+
## What's Affected
220+
221+
**Components:** [DtBadge](/components/badge.html), [DtButton](/components/button.html), [DtInput](/components/input.html), [DtTab](/components/tab-group.html), [DtSplitButton](/components/split-button.html), [DtItemLayout](/components/item-layout.html), [DtRootLayout](/components/root-layout.html)
222+
223+
**Recipes:** Callbox, Contact Centers Row, General Row, Top Banner Info, Grouped Chip
224+
225+
## ESLint Rule
226+
227+
`deprecated-physical-naming` flags all deprecated physical slot, prop, prop value, and event usage on Dialtone components.
228+
229+
Add to your ESLint config:
230+
231+
```js
232+
// eslint.config.js (flat config)
233+
import dialtone from '@dialpad/eslint-plugin-dialtone';
234+
235+
export default [
236+
{
237+
plugins: { dialtone },
238+
rules: {
239+
'dialtone/deprecated-physical-naming': 'warn',
240+
},
241+
},
242+
];
243+
```
244+
245+
## Migration Script
246+
247+
Run the migration helper from your project root:
248+
249+
```bash
250+
npx dialtone-migration-helper --cwd ./src
251+
```
252+
253+
Select **"physical-to-logical"** from the config list. This renames all unambiguous physical names to their logical equivalents across `.vue`, `.md`, `.html`, `.js`, `.ts`, `.jsx`, and `.tsx` files.
254+
255+
To apply changes without interactive confirmation:
256+
257+
```bash
258+
npx dialtone-migration-helper --cwd ./src --force
259+
```
260+
261+
**The script handles:** slot directives, prop names, prop values, and event listeners for all affected components.
262+
263+
**Skipped:** The `#icon` slot on `dt-button` (ambiguous), dynamic bindings, and script-block references.
264+
265+
Thanks! -- Dialtone Team
266+
267+
</BlogPost>
268+
269+
<script setup>
270+
import BlogPost from '@baseComponents/BlogPost.vue';
271+
import { parse } from 'date-fns';
272+
</script>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Migration: deprecated physical direction slot/prop/event names → logical equivalents.
2+
// Covers Vue template directives only. Does NOT cover:
3+
// - #icon on dt-button (ambiguous — requires manual migration)
4+
// - Dynamic bindings or script-section references
5+
6+
export default {
7+
description:
8+
'Renames deprecated physical direction names (left/right/top/bottom/alpha/omega) to\n' +
9+
'logical equivalents (start/end/blockStart/blockEnd) in Vue template slots, props,\n' +
10+
'prop values, and events. Does NOT rename #icon on dt-button (ambiguous).',
11+
patterns: ['**/*.{vue,html,md,js,ts,jsx,tsx}'],
12+
expressions: [
13+
// ── Slot renames ──────────────────────────────────────────────────────
14+
// Longer patterns first to prevent partial matches.
15+
// e.g. #leftIcon before #left, #rightIcon before #right
16+
17+
// #leftIcon → #startIcon
18+
{ from: /#leftIcon/g, to: '#startIcon' },
19+
// #rightIcon → #endIcon
20+
{ from: /#rightIcon/g, to: '#endIcon' },
21+
// #alphaIcon → #startIcon
22+
{ from: /#alphaIcon/g, to: '#startIcon' },
23+
// #omegaIcon → #endIcon
24+
{ from: /#omegaIcon/g, to: '#endIcon' },
25+
// #leftContent → #startContent
26+
{ from: /#leftContent/g, to: '#startContent' },
27+
// #rightContent → #endContent
28+
{ from: /#rightContent/g, to: '#endContent' },
29+
// #omega → #end (word boundary to avoid matching #omegaIcon)
30+
// Scoped: only dt-split-button uses #omega
31+
{ from: /#omega(?=[\s"'>])/g, to: '#end' },
32+
33+
// Generic short slot names (#left, #right, #bottom) are scoped to known
34+
// Dialtone components to avoid renaming slots on non-Dialtone components.
35+
// Uses multiline matching to find the slot within a dt-* or dt-recipe-* tag.
36+
// Components: dt-item-layout, dt-recipe-callbox, dt-recipe-contact-centers-row,
37+
// dt-recipe-general-row, dt-recipe-top-banner-info
38+
39+
// #left → #start (only on dt-item-layout, dt-recipe-general-row, dt-recipe-top-banner-info)
40+
{ from: /(<dt-(?:item-layout|recipe-general-row|recipe-top-banner-info)[\s\S]*?)#left(?=[\s"'>])/gm, to: '$1#start' },
41+
// #right → #end (only on dt-item-layout, dt-recipe-callbox, dt-recipe-contact-centers-row, dt-recipe-top-banner-info)
42+
{ from: /(<dt-(?:item-layout|recipe-callbox|recipe-contact-centers-row|recipe-top-banner-info)[\s\S]*?)#right(?=[\s"'>])/gm, to: '$1#end' },
43+
// #bottom → #blockEnd (only on dt-item-layout, dt-recipe-callbox)
44+
{ from: /(<dt-(?:item-layout|recipe-callbox)[\s\S]*?)#bottom(?=[\s"'>])/gm, to: '$1#blockEnd' },
45+
46+
// ── Prop renames ──────────────────────────────────────────────────────
47+
// Longer patterns first within each prefix group.
48+
49+
// alpha-* → start-* (longest first)
50+
{ from: /alpha-trailing-class/g, to: 'start-trailing-class' },
51+
{ from: /alpha-tooltip-text/g, to: 'start-tooltip-text' },
52+
{ from: /alpha-leading-class/g, to: 'start-leading-class' },
53+
{ from: /alpha-icon-position/g, to: 'start-icon-position' },
54+
{ from: /alpha-aria-label/g, to: 'start-aria-label' },
55+
{ from: /alpha-label-class/g, to: 'start-label-class' },
56+
{ from: /alpha-disabled/g, to: 'start-disabled' },
57+
{ from: /alpha-loading/g, to: 'start-loading' },
58+
{ from: /alpha-active/g, to: 'start-active' },
59+
60+
// omega-* → end-* (longest first)
61+
{ from: /omega-tooltip-text/g, to: 'end-tooltip-text' },
62+
{ from: /omega-aria-label/g, to: 'end-aria-label' },
63+
{ from: /omega-disabled/g, to: 'end-disabled' },
64+
{ from: /omega-active/g, to: 'end-active' },
65+
{ from: /omega-id/g, to: 'end-id' },
66+
67+
// layout class props
68+
{ from: /bottom-class=/g, to: 'block-end-class=' },
69+
{ from: /left-class=/g, to: 'start-class=' },
70+
{ from: /right-class=/g, to: 'end-class=' },
71+
72+
// ── Prop value renames ────────────────────────────────────────────────
73+
// icon-position values
74+
{ from: /icon-position="left"/g, to: 'icon-position="start"' },
75+
{ from: /icon-position="right"/g, to: 'icon-position="end"' },
76+
{ from: /icon-position="top"/g, to: 'icon-position="blockStart"' },
77+
{ from: /icon-position="bottom"/g, to: 'icon-position="blockEnd"' },
78+
// sidebar-position values
79+
{ from: /sidebar-position="left"/g, to: 'sidebar-position="start"' },
80+
{ from: /sidebar-position="right"/g, to: 'sidebar-position="end"' },
81+
82+
// ── Event renames ─────────────────────────────────────────────────────
83+
{ from: /@alpha-clicked/g, to: '@start-clicked' },
84+
{ from: /@omega-clicked/g, to: '@end-clicked' },
85+
],
86+
};

0 commit comments

Comments
 (0)