Skip to content

Commit 10d09aa

Browse files
committed
feat: add support custom color shades for primary and neutral colors in theme configuration
1 parent ce460bb commit 10d09aa

File tree

4 files changed

+135
-12
lines changed

4 files changed

+135
-12
lines changed

src/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export type {
151151
StateColors,
152152
PrimaryColors,
153153
NeutralColors,
154+
ColorShades,
154155
VuelessCssVariables,
155156
/* Component and Directive types */
156157
Directives,

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export type {
157157
StateColors,
158158
PrimaryColors,
159159
NeutralColors,
160+
ColorShades,
160161
VuelessCssVariables,
161162
/* Component and Directive types */
162163
Directives,

src/types.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,28 @@ export type StateColors =
220220
| "grayscale"
221221
| string;
222222

223-
export type NeutralColors = "slate" | "gray" | "zinc" | "neutral" | "stone" | string;
223+
export interface ColorShades {
224+
50: string;
225+
100: string;
226+
200: string;
227+
300: string;
228+
400: string;
229+
500: string;
230+
600: string;
231+
700: string;
232+
800: string;
233+
900: string;
234+
950: string;
235+
}
236+
237+
export type NeutralColors =
238+
| "slate"
239+
| "gray"
240+
| "zinc"
241+
| "neutral"
242+
| "stone"
243+
| string
244+
| Partial<ColorShades>;
224245
export type PrimaryColors =
225246
| "red"
226247
| "orange"
@@ -239,7 +260,8 @@ export type PrimaryColors =
239260
| "fuchsia"
240261
| "pink"
241262
| "rose"
242-
| string;
263+
| string
264+
| Partial<ColorShades>;
243265

244266
export interface Directives {
245267
tooltip?: Partial<Props>;

src/utils/theme.ts

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import type {
4545
NeutralColors,
4646
PrimaryColors,
4747
ThemeConfig,
48+
ColorShades,
4849
ThemeConfigText,
4950
ThemeConfigOutline,
5051
ThemeConfigRounding,
@@ -359,6 +360,40 @@ export function normalizeThemeConfig(theme: any): MergedThemeConfig {
359360
};
360361
}
361362

363+
function isColorShadesObject(color: unknown): color is Partial<ColorShades> {
364+
return typeof color === "object" && color !== null && !Array.isArray(color);
365+
}
366+
367+
function validateColorShades(color: Partial<ColorShades>, colorType: string) {
368+
const validShades = COLOR_SHADES;
369+
const providedShades = Object.keys(color);
370+
371+
const invalidShades = providedShades.filter((shade) => !validShades.includes(Number(shade)));
372+
const missingShades = validShades.filter((shade) => !(String(shade) in color));
373+
374+
if (invalidShades.length > 0) {
375+
const invalidShadesStr = invalidShades.join(", ");
376+
const validShadesStr = validShades.join(", ");
377+
378+
// eslint-disable-next-line no-console
379+
console.warn(
380+
`[vueless] Invalid shade keys found in ${colorType} color object: ${invalidShadesStr}. ` +
381+
`Valid shades are: ${validShadesStr}.`,
382+
);
383+
}
384+
385+
if (missingShades.length > 0) {
386+
const missingShadesStr = missingShades.join(", ");
387+
const validShadesStr = validShades.join(", ");
388+
389+
// eslint-disable-next-line no-console
390+
console.warn(
391+
`[vueless] Missing shade keys in ${colorType} color object: ${missingShadesStr}. ` +
392+
`All shades (${validShadesStr}) should be defined.`,
393+
);
394+
}
395+
}
396+
362397
/**
363398
* Determines if the provided color mode configuration has a primary color
364399
* that differs from the default color mode configuration.
@@ -377,13 +412,35 @@ function hasPrimaryColor(
377412

378413
/**
379414
* Retrieve primary color value and save them to cookie and localStorage.
380-
* @return string - primary color.
415+
* @return string | Partial<ColorShades> - primary color.
381416
*/
382417
function getPrimaryColor(primary?: PrimaryColors) {
383418
const storageKey = `vl-${PRIMARY_COLOR}`;
384419

420+
const storedValue = getStored(storageKey);
421+
let parsedStoredValue: PrimaryColors | undefined;
422+
423+
if (storedValue) {
424+
try {
425+
parsedStoredValue = JSON.parse(storedValue);
426+
} catch {
427+
parsedStoredValue = storedValue;
428+
}
429+
}
430+
385431
let primaryColor: PrimaryColors =
386-
primary ?? getStored(storageKey) ?? vuelessConfig.primary ?? DEFAULT_PRIMARY_COLOR;
432+
primary ?? parsedStoredValue ?? vuelessConfig.primary ?? DEFAULT_PRIMARY_COLOR;
433+
434+
if (isColorShadesObject(primaryColor)) {
435+
validateColorShades(primaryColor, PRIMARY_COLOR);
436+
437+
if (isCSR && primary) {
438+
setCookie(storageKey, JSON.stringify(primaryColor));
439+
localStorage.setItem(storageKey, JSON.stringify(primaryColor));
440+
}
441+
442+
return primaryColor;
443+
}
387444

388445
const isPrimaryColor =
389446
PRIMARY_COLORS.some((color) => color === primaryColor) || primaryColor === GRAYSCALE_COLOR;
@@ -405,13 +462,35 @@ function getPrimaryColor(primary?: PrimaryColors) {
405462

406463
/**
407464
* Retrieve neutral color value and save them to cookie and localStorage.
408-
* @return string - neutral color.
465+
* @return string | Partial<ColorShades> - neutral color.
409466
*/
410467
function getNeutralColor(neutral?: NeutralColors) {
411468
const storageKey = `vl-${NEUTRAL_COLOR}`;
412469

470+
const storedValue = getStored(storageKey);
471+
let parsedStoredValue: NeutralColors | undefined;
472+
473+
if (storedValue) {
474+
try {
475+
parsedStoredValue = JSON.parse(storedValue);
476+
} catch {
477+
parsedStoredValue = storedValue;
478+
}
479+
}
480+
413481
let neutralColor: NeutralColors =
414-
neutral ?? getStored(storageKey) ?? vuelessConfig.neutral ?? DEFAULT_NEUTRAL_COLOR;
482+
neutral ?? parsedStoredValue ?? vuelessConfig.neutral ?? DEFAULT_NEUTRAL_COLOR;
483+
484+
if (isColorShadesObject(neutralColor)) {
485+
validateColorShades(neutralColor, NEUTRAL_COLOR);
486+
487+
if (isCSR && neutral) {
488+
setCookie(storageKey, JSON.stringify(neutralColor));
489+
localStorage.setItem(storageKey, JSON.stringify(neutralColor));
490+
}
491+
492+
return neutralColor;
493+
}
415494

416495
const isNeutralColor = NEUTRAL_COLORS.some((color) => color === neutralColor);
417496

@@ -733,14 +812,34 @@ export function setRootCSSVariables(vars: MergedThemeConfig) {
733812
"--vl-disabled-opacity": `${vars.disabledOpacity}%`,
734813
};
735814

736-
for (const shade of COLOR_SHADES) {
737-
variables[`--vl-${PRIMARY_COLOR}-${shade}` as keyof VuelessCssVariables] =
738-
`var(--color-${vars.primary}-${shade})`;
815+
if (isColorShadesObject(vars.primary)) {
816+
for (const shade of COLOR_SHADES) {
817+
const shadeValue = vars.primary[shade as keyof ColorShades];
818+
819+
if (shadeValue) {
820+
variables[`--vl-${PRIMARY_COLOR}-${shade}` as keyof VuelessCssVariables] = shadeValue;
821+
}
822+
}
823+
} else {
824+
for (const shade of COLOR_SHADES) {
825+
variables[`--vl-${PRIMARY_COLOR}-${shade}` as keyof VuelessCssVariables] =
826+
`var(--color-${vars.primary}-${shade})`;
827+
}
739828
}
740829

741-
for (const shade of COLOR_SHADES) {
742-
variables[`--vl-${NEUTRAL_COLOR}-${shade}` as keyof VuelessCssVariables] =
743-
`var(--color-${vars.neutral}-${shade})`;
830+
if (isColorShadesObject(vars.neutral)) {
831+
for (const shade of COLOR_SHADES) {
832+
const shadeValue = vars.neutral[shade as keyof ColorShades];
833+
834+
if (shadeValue) {
835+
variables[`--vl-${NEUTRAL_COLOR}-${shade}` as keyof VuelessCssVariables] = shadeValue;
836+
}
837+
}
838+
} else {
839+
for (const shade of COLOR_SHADES) {
840+
variables[`--vl-${NEUTRAL_COLOR}-${shade}` as keyof VuelessCssVariables] =
841+
`var(--color-${vars.neutral}-${shade})`;
842+
}
744843
}
745844

746845
const [light, dark] = generateCSSColorVariables(vars.lightTheme ?? {}, vars.darkTheme ?? {});

0 commit comments

Comments
 (0)