-
Notifications
You must be signed in to change notification settings - Fork 7
docs: DLT-3151 blog post for ui-kits migration and script #1156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
84881b2
docs: DLT-3151 blog post for ui-kits migration, and script
braddialpad 87acde1
remove private repo ref
braddialpad a5f7399
improve script
braddialpad 03c9886
update doc
braddialpad a385053
clean doc
braddialpad d502bf9
add type support
braddialpad File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
100 changes: 100 additions & 0 deletions
100
apps/dialtone-documentation/docs/about/whats-new/posts/2026-3-26.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| (_, 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() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import-rewrite regex only matches
import { ... } from '@dialpad/dialtone/vue3', soimport type { ... }statements are skipped; the later global symbol replacement still renamesDtRecipe*to the new names, leaving type imports pointed at the old package (for exampleimport 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 πΒ / π.