Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
21 changes: 14 additions & 7 deletions apps/studio/src/lib/editor/engine/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ColorItem } from '@/routes/editor/LayersPanel/BrandTab/ColorPanel/
import { DEFAULT_COLOR_NAME, MainChannels } from '@onlook/models';
import type { ConfigResult, ParsedColors, ThemeColors } from '@onlook/models/assets';
import { Theme } from '@onlook/models/assets';
import { Color } from '@onlook/utility';
import { Color, generateUniqueName } from '@onlook/utility';
import { makeAutoObservable } from 'mobx';
import colors from 'tailwindcss/colors';
import type { EditorEngine } from '..';
Expand Down Expand Up @@ -451,12 +451,19 @@ export class ThemeManager {
if (!colorToDuplicate) {
throw new Error('Color not found');
}
// If the color name is a number, we need to add a suffix to the new color name
const randomId = customAlphabet('0123456789', 5)();
const randomText = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5)();
const newName = isNaN(Number(colorName))
? `${colorName}Copy${randomText}`
: `${colorName}${randomId}`;

// Generate a unique name for the duplicated color
const existingNames = group.map((color) => color.name);
let newName: string;

if (isNaN(Number(colorName))) {
// For non-numeric names, use the generateUniqueName utility
newName = generateUniqueName(colorName, existingNames);
} else {
// For numeric names, generate a random numeric suffix
const randomId = customAlphabet('0123456789', 5)();
newName = `${colorName}${randomId}`;
}

const color = Color.from(
theme === Theme.DARK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ import {
} from '@onlook/ui/dropdown-menu';
import { Icons } from '@onlook/ui/icons';
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from '@onlook/ui/tooltip';
import { Color, toNormalCase } from '@onlook/utility';
import { Color, toNormalCase, generateUniqueName } from '@onlook/utility';
import { useState } from 'react';
import { ColorPopover } from './ColorPopover';
import { ColorNameInput } from './ColorNameInput';
import { customAlphabet } from 'nanoid/non-secure';

export interface ColorItem {
name: string;
originalKey: string;
Expand Down Expand Up @@ -125,10 +123,7 @@ export const BrandPalletGroup = ({
};

const generateUniqueColorName = () => {
const randomIdText = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5)();
const randomIdNumber = customAlphabet('0123456789', 5)();
const randomId = isNaN(Number(title)) ? randomIdText : randomIdNumber;
return `${title} ${randomId}`;
return generateUniqueName(title, existedName);
};

return (
Expand Down
28 changes: 28 additions & 0 deletions packages/utility/src/string.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { camelCase } from 'lodash';

export function isEmptyString(str: string): boolean {
return str.trim() === '';
}
Expand Down Expand Up @@ -32,3 +34,29 @@ export function toNormalCase(str: string): string {
export function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

/**
* Generates a unique name by appending a number to the base name until it doesn't conflict with existing names.
* The comparison is done using camelCase to ensure consistent name formatting.
* @param baseName The base name to start with
* @param existingNames Array of existing names to check against
* @param transformFn Optional function to transform the name before comparison (defaults to camelCase)
* @returns A unique name that doesn't conflict with existing names
*/
export function generateUniqueName(
baseName: string,
existingNames: string[],
transformFn: (str: string) => string = camelCase,
): string {
let counter = 1;
let newName = `${baseName} ${counter}`;
let transformedName = transformFn(newName);

while (existingNames.includes(transformedName)) {
counter++;
newName = `${baseName} ${counter}`;
transformedName = transformFn(newName);
}

return newName;
}
74 changes: 74 additions & 0 deletions packages/utility/test/string.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { generateUniqueName } from '@onlook/utility';

describe('generateUniqueName', () => {
it('should generate a unique name when no existing names', () => {
const baseName = 'Primary';
const existingNames: string[] = [];
const result = generateUniqueName(baseName, existingNames);
expect(result).toBe('Primary 1');
});

it('should generate a unique name when name exists', () => {
const baseName = 'Primary';
const existingNames = ['primary'];
const result = generateUniqueName(baseName, existingNames);
expect(result).toBe('Primary 1');
});

it('should increment counter when multiple names exist', () => {
const baseName = 'Primary';
const existingNames = ['primary', 'primary1', 'primary2'];
const result = generateUniqueName(baseName, existingNames);
expect(result).toBe('Primary 3');
});

it('should handle names with spaces', () => {
const baseName = 'Primary Color';
const existingNames = ['primaryColor'];
const result = generateUniqueName(baseName, existingNames);
expect(result).toBe('Primary Color 1');
});

it('should handle special characters in base name', () => {
const baseName = 'Primary-Color';
const existingNames = ['primaryColor'];
const result = generateUniqueName(baseName, existingNames);
expect(result).toBe('Primary-Color 1');
});

it('should handle empty base name', () => {
const baseName = '';
const existingNames: string[] = [];
const result = generateUniqueName(baseName, existingNames);
expect(result).toBe(' 1');
});

it('should work with custom transform function', () => {
const baseName = 'Primary';
const existingNames = ['PRIMARY'];
const customTransform = (str: string) => str.toUpperCase();
const result = generateUniqueName(baseName, existingNames, customTransform);
expect(result).toBe('Primary 1');
});

it('should handle numeric base names', () => {
const baseName = '100';
const existingNames = ['100'];
const result = generateUniqueName(baseName, existingNames);
expect(result).toBe('100 1');
});

it('should handle mixed case existing names', () => {
const baseName = 'Primary';
const existingNames = ['PRIMARY', 'primary', 'Primary'];
const result = generateUniqueName(baseName, existingNames);
expect(result).toBe('Primary 1');
});

it('should handle consecutive numbers in existing names', () => {
const baseName = 'Primary';
const existingNames = ['primary', 'primary1', 'primary3', 'primary4'];
const result = generateUniqueName(baseName, existingNames);
expect(result).toBe('Primary 2');
});
});