Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions apps/dialtone-documentation/docs/about/whats-new/posts/2026-3-26.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
heading: 'Dialtone recipes have been migrated to UI-Kits'
author: Brad Paugh
posted: '2026-3-26'
description: Dialtone recipes have been deprecated and moved to the UI-Kits repository.
---

<BlogPost :author="$frontmatter.author" :posted="parse($frontmatter.posted, 'y-M-d', new Date())" :heading="$frontmatter.heading">

Hello everyone, the Dialtone team here with an important update, We'll be deprecating all `DtRecipe*` components in Dialtone in favor of standalone UI-Kit packages. Additionally a few recipes have been upgraded from recipes to core dialtone components.

Each kit is individually published to npm and can be installed independently:

\```
npm install @dialpad/callbarkit
npm install @dialpad/chatkit
npm install @dialpad/formkit
npm install @dialpad/navigationkit
npm install @dialpad/workflowkit
\```

You may see the storybook for UI-Kits here: TBD

Here are the mappings from old components to new components. They are all functionally the same, however the deprecated DtRecipe components will no longer receieve updates.

**Full migration mapping:**

| Old (DtRecipe) | New | Package |
|----------------|-----|---------|
| `DtRecipeComboboxMultiSelect` | `DtComboboxMultiSelect` | `@dialpad/dialtone/vue3` |
| `DtRecipeComboboxWithPopover` | `DtComboboxWithPopover` | `@dialpad/dialtone/vue3` |
| `DtRecipeMotionText` | `DtMotionText` | `@dialpad/dialtone/vue3` |
| `DtRecipeCallbarButton` | `DpCallbarButton` | `@dialpad/callbarkit/vue3` |
| `DtRecipeCallbarButtonWithPopover` | `DpCallbarButtonWithPopover` | `@dialpad/callbarkit/vue3` |
| `DtRecipeCallbarButtonWithDropdown` | `DpCallbarButtonWithDropdown` | `@dialpad/callbarkit/vue3` |
| `DtRecipeGroupedChip` | `DpGroupedChip` | `@dialpad/callbarkit/vue3` |
| `DtRecipeTopBannerInfo` | `DpTopBannerInfo` | `@dialpad/callbarkit/vue3` |
| `DtRecipeAttachmentCarousel` | `DpAttachmentCarousel` | `@dialpad/chatkit/vue3` |
| `DtRecipeMessageInput` | `DpMessageInput` | `@dialpad/chatkit/vue3` |
| `DtRecipeContactInfo` | `DpContactInfo` | `@dialpad/chatkit/vue3` |
| `DtRecipeEditor` | `DpEditor` | `@dialpad/chatkit/vue3` |
| `DtRecipeEmojiRow` | `DpEmojiRow` | `@dialpad/chatkit/vue3` |
| `DtRecipeFeedItemPill` | `DpFeedItemPill` | `@dialpad/chatkit/vue3` |
| `DtRecipeFeedItemRow` | `DpFeedItemRow` | `@dialpad/chatkit/vue3` |
| `DtRecipeContactCentersRow` | `DtContactCentersRow` | `@dialpad/navigationkit/vue3` |
| `DtRecipeContactRow` | `DtContactRow` | `@dialpad/navigationkit/vue3` |
| `DtRecipeGeneralRow` | `DtGeneralRow` | `@dialpad/navigationkit/vue3` |
| `DtRecipeGroupRow` | `DtGroupRow` | `@dialpad/navigationkit/vue3` |
| `DtRecipeUnreadPill` | `DtUnreadPill` | `@dialpad/navigationkit/vue3` |
| `DtRecipeCallbox` | `DtCallbox` | `@dialpad/navigationkit/vue3` |
| `DtRecipeSettingsMenuButton` | `DtSettingsMenuButton` | `@dialpad/navigationkit/vue3` |
| `DtRecipeIvrNode` | `DtIvrNode` | `@dialpad/workflowkit/vue3` |

CSS classes also change: `dt-recipe-*` β†’ `dp-*`

**Timeline:**

- `DtRecipe*` components will remain available until the next major Dialtone release (Q2 2026), but **will no longer receive updates**
- Migrate before then to stay current with improvements and bug fixes

**Automated migration script:**

We provide a Node.js script that automatically updates your codebase β€” no manual find-and-replace needed. It handles:

- Import statement rewrites (splits `@dialpad/dialtone/vue3` imports into the correct new packages)
- PascalCase component name replacements in JS/TS/Vue script blocks
- Kebab-case component name replacements in Vue templates
- CSS class prefix changes (`dt-recipe-*` β†’ `dp-*` / `dt-*`)

Download and run it with:

\```sh

# Preview changes without writing files

curl -s <https://raw.githubusercontent.com/dialpad/dialtone/staging/scripts/migrate-recipes-to-uikits.mjs> | node - /path/to/your/project --dry-run

# Apply changes

curl -s <https://raw.githubusercontent.com/dialpad/dialtone/staging/scripts/migrate-recipes-to-uikits.mjs> | node - /path/to/your/project
\```

After running, install the new packages your project needs and verify with your lint + test suite.

This migration has already been performed on the Dialpad application.

**What you need to do:**

- Use the mappings above for any new code
- Any changes previously made to `DtRecipe*` components in Dialtone should now be made in [dialpad-uikits](https://github.com/dialpad/dialpad-uikits) instead
- See [DLT-3063](https://github.com/dialpad/firespotter/pull/72240) for migration examples

Thanks! Please let us know in #dialtone if you have any issues.

</BlogPost>

<script setup>
import BlogPost from '@baseComponents/BlogPost.vue';
import { parse } from 'date-fns';
</script>
229 changes: 229 additions & 0 deletions scripts/migrate-recipes-to-uikits.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
#!/usr/bin/env node

/**
* migrate-recipes-to-uikits.mjs
*
* Migrates DtRecipe* components from @dialpad/dialtone to UI-Kit packages.
* See: https://dialtone.dialpad.com/about/whats-new/
*
* Usage:
* node scripts/migrate-recipes-to-uikits.mjs [directory] [--dry-run]
*
* Options:
* directory Root directory to scan (default: current working directory)
* --dry-run Report changes without writing files
*
* Handles:
* - Renamed PascalCase components in JS/TS/Vue script blocks
* - Renamed kebab-case components in Vue templates
* - CSS class prefix changes (dt-recipe-* β†’ dp-* / dt-*)
* - Import statement rewrites (splits by new package)
*/

import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs'
import { join, extname, relative } from 'path'

// ─── Migration map ────────────────────────────────────────────────────────────

const MIGRATION_MAP = {
// Promoted to core Dialtone
DtRecipeComboboxMultiSelect: { newName: 'DtComboboxMultiSelect', newPackage: '@dialpad/dialtone/vue3' },
DtRecipeComboboxWithPopover: { newName: 'DtComboboxWithPopover', newPackage: '@dialpad/dialtone/vue3' },
DtRecipeMotionText: { newName: 'DtMotionText', newPackage: '@dialpad/dialtone/vue3' },
// callbarkit
DtRecipeCallbarButton: { newName: 'DpCallbarButton', newPackage: '@dialpad/callbarkit/vue3' },
DtRecipeCallbarButtonWithPopover: { newName: 'DpCallbarButtonWithPopover', newPackage: '@dialpad/callbarkit/vue3' },
DtRecipeCallbarButtonWithDropdown: { newName: 'DpCallbarButtonWithDropdown', newPackage: '@dialpad/callbarkit/vue3' },
DtRecipeGroupedChip: { newName: 'DpGroupedChip', newPackage: '@dialpad/callbarkit/vue3' },
DtRecipeTopBannerInfo: { newName: 'DpTopBannerInfo', newPackage: '@dialpad/callbarkit/vue3' },
// chatkit
DtRecipeAttachmentCarousel: { newName: 'DpAttachmentCarousel', newPackage: '@dialpad/chatkit/vue3' },
DtRecipeMessageInput: { newName: 'DpMessageInput', newPackage: '@dialpad/chatkit/vue3' },
DtRecipeContactInfo: { newName: 'DpContactInfo', newPackage: '@dialpad/chatkit/vue3' },
DtRecipeEditor: { newName: 'DpEditor', newPackage: '@dialpad/chatkit/vue3' },
DtRecipeEmojiRow: { newName: 'DpEmojiRow', newPackage: '@dialpad/chatkit/vue3' },
DtRecipeFeedItemPill: { newName: 'DpFeedItemPill', newPackage: '@dialpad/chatkit/vue3' },
DtRecipeFeedItemRow: { newName: 'DpFeedItemRow', newPackage: '@dialpad/chatkit/vue3' },
// navigationkit
DtRecipeContactCentersRow: { newName: 'DtContactCentersRow', newPackage: '@dialpad/navigationkit/vue3' },
DtRecipeContactRow: { newName: 'DtContactRow', newPackage: '@dialpad/navigationkit/vue3' },
DtRecipeGeneralRow: { newName: 'DtGeneralRow', newPackage: '@dialpad/navigationkit/vue3' },
DtRecipeGroupRow: { newName: 'DtGroupRow', newPackage: '@dialpad/navigationkit/vue3' },
DtRecipeUnreadPill: { newName: 'DtUnreadPill', newPackage: '@dialpad/navigationkit/vue3' },
DtRecipeCallbox: { newName: 'DtCallbox', newPackage: '@dialpad/navigationkit/vue3' },
DtRecipeSettingsMenuButton: { newName: 'DtSettingsMenuButton', newPackage: '@dialpad/navigationkit/vue3' },
// workflowkit
DtRecipeIvrNode: { newName: 'DtIvrNode', newPackage: '@dialpad/workflowkit/vue3' },
}

// ─── Utilities ────────────────────────────────────────────────────────────────

/** Convert PascalCase to kebab-case. e.g. DtRecipeCallbarButton β†’ dt-recipe-callbar-button */
function toKebabCase (pascal) {
return pascal.replace(/([A-Z])/g, (char, _, offset) =>
offset === 0 ? char.toLowerCase() : '-' + char.toLowerCase(),
)
}

/** Build a CSS-class-level map from the migration map. */
const CSS_CLASS_MAP = Object.fromEntries(
Object.entries(MIGRATION_MAP).map(([old, { newName }]) => [
toKebabCase(old),
toKebabCase(newName),
]),
)

// Escape a string for use inside a RegExp literal
function escapeRegExp (str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

// ─── File extensions to process ───────────────────────────────────────────────

const PROCESSABLE_EXTENSIONS = new Set(['.vue', '.js', '.ts', '.jsx', '.tsx'])

// Directories to skip entirely
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.nuxt', '.output', 'coverage'])

// ─── Transform logic ─────────────────────────────────────────────────────────

/**
* Rewrite a single named-import statement from @dialpad/dialtone/vue3.
* Splits recipe imports out into their new packages.
*/
function rewriteDialtoneImport (importedNames, quote) {
const names = importedNames.split(',').map(n => n.trim()).filter(Boolean)

/** @type {Record<string, string[]>} package β†’ list of import specifiers */
const byPackage = {}

for (const specifier of names) {
// Handle `Name as Alias` form
const [importedName, alias] = specifier.split(/\s+as\s+/).map(s => s.trim())
const migration = MIGRATION_MAP[importedName]
const targetPkg = migration ? migration.newPackage : '@dialpad/dialtone/vue3'
const targetName = migration ? migration.newName : importedName
const entry = alias ? `${targetName} as ${alias}` : targetName

if (!byPackage[targetPkg]) byPackage[targetPkg] = []
byPackage[targetPkg].push(entry)
}

// Stable order: dialtone first, then kits alphabetically
const sorted = Object.entries(byPackage).sort(([a], [b]) => {
if (a === '@dialpad/dialtone/vue3') return -1
if (b === '@dialpad/dialtone/vue3') return 1
return a.localeCompare(b)
})

return sorted
.map(([pkg, pkgNames]) => `import { ${pkgNames.join(', ')} } from ${quote}${pkg}${quote}`)
.join('\n')
}

/**
* Apply all migrations to the given file content string.
* Returns the new content, or null if nothing changed.
*/
function migrateContent (content) {
let result = content

// 1. Rewrite named imports from @dialpad/dialtone/vue3
result = result.replace(
/import\s*\{([^}]+)\}\s*from\s*(['"])@dialpad\/dialtone\/vue3\2/g,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Rewrite type-only Dialtone imports during migration

The import-rewrite regex only matches import { ... } from '@dialpad/dialtone/vue3', so import type { ... } statements are skipped; the later global symbol replacement still renames DtRecipe* to the new names, leaving type imports pointed at the old package (for example import type { DpCallbarButton } from '@dialpad/dialtone/vue3'). In TypeScript codebases that use type-only imports, this produces incorrect package references and can fail type-check/build after running the script.

Useful? React with πŸ‘Β / πŸ‘Ž.

(_, importedNames, quote) => rewriteDialtoneImport(importedNames, quote),
)

// 2. Replace PascalCase component names (whole-word, handles JS/TS/JSX and Vue templates)
// Process longer names first to avoid partial replacements (e.g. CallbarButton before Button)
const sortedEntries = Object.entries(MIGRATION_MAP).sort(
([a], [b]) => b.length - a.length,
)
for (const [oldName, { newName }] of sortedEntries) {
result = result.replace(new RegExp(`\\b${escapeRegExp(oldName)}\\b`, 'g'), newName)
}

// 3. Replace kebab-case component names in templates (e.g. <dt-recipe-callbar-button>)
// Also sort longest first to prevent partial collisions
const sortedCssEntries = Object.entries(CSS_CLASS_MAP).sort(
([a], [b]) => b.length - a.length,
)
for (const [oldClass, newClass] of sortedCssEntries) {
if (oldClass !== newClass) {
result = result.replace(new RegExp(escapeRegExp(oldClass), 'g'), newClass)
}
}

return result === content ? null : result
}

// ─── File walking ─────────────────────────────────────────────────────────────

function* walkFiles (dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!SKIP_DIRS.has(entry.name)) yield* walkFiles(join(dir, entry.name))
} else if (entry.isFile() && PROCESSABLE_EXTENSIONS.has(extname(entry.name))) {
yield join(dir, entry.name)
}
}
}

// ─── Main ─────────────────────────────────────────────────────────────────────

function main () {
const args = process.argv.slice(2)
const dryRun = args.includes('--dry-run')
const targetDir = args.find(a => !a.startsWith('--')) ?? process.cwd()

let targetStat
try {
targetStat = statSync(targetDir)
} catch {
console.error(`Error: directory not found β€” ${targetDir}`)
process.exit(1)
}
if (!targetStat.isDirectory()) {
console.error(`Error: not a directory β€” ${targetDir}`)
process.exit(1)
}

console.log(`Scanning ${targetDir}${dryRun ? ' (dry run)' : ''}…\n`)

const changedFiles = []
const skippedFiles = []

for (const filePath of walkFiles(targetDir)) {
let content
try {
content = readFileSync(filePath, 'utf8')
} catch (err) {
console.warn(` SKIP ${relative(targetDir, filePath)} β€” read error: ${err.message}`)
skippedFiles.push(filePath)
continue
}

const migrated = migrateContent(content)
if (migrated === null) continue

changedFiles.push(filePath)
console.log(` ${dryRun ? 'WOULD UPDATE' : 'UPDATED'} ${relative(targetDir, filePath)}`)

if (!dryRun) {
writeFileSync(filePath, migrated, 'utf8')
}
}

console.log(`\n${dryRun ? '[Dry run] ' : ''}${changedFiles.length} file(s) ${dryRun ? 'would be' : 'were'} updated.`)

if (changedFiles.length > 0) {
console.log('\nNext steps:')
console.log(' 1. Install new packages in your project:')
console.log(' npm install @dialpad/callbarkit @dialpad/chatkit @dialpad/navigationkit @dialpad/workflowkit')
console.log(' 2. Review the diff and verify component prop APIs match the new package docs.')
console.log(' 3. Run your lint + test suite to catch any remaining issues.')
console.log(' 4. See DLT-3063 for migration examples: https://github.com/dialpad/firespotter/pull/72240')
}
}

main()
Loading