feat(projects): 🎉 init project

This commit is contained in:
skyfeiz
2024-12-01 21:32:54 +08:00
parent b52dd15b36
commit cd8d32ca2b
345 changed files with 30041 additions and 10953 deletions

View File

@@ -0,0 +1,17 @@
{
"name": "@sa/alova",
"version": "0.1.0",
"exports": {
".": "./src/index.ts",
"./client": "./src/client.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/utils": "workspace:*",
"alova": "3.0.20"
}
}

View File

@@ -0,0 +1 @@
export * from 'alova/client';

View File

@@ -0,0 +1,2 @@
/** the backend error code key */
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';

View File

@@ -0,0 +1,77 @@
import { createAlova } from 'alova';
import type { AlovaDefaultCacheAdapter, AlovaGenerics, AlovaGlobalCacheAdapter, AlovaRequestAdapter } from 'alova';
import VueHook from 'alova/vue';
import type { VueHookType } from 'alova/vue';
import adapterFetch from 'alova/fetch';
import { createServerTokenAuthentication } from 'alova/client';
import type { FetchRequestInit } from 'alova/fetch';
import { BACKEND_ERROR_CODE } from './constant';
import type { CustomAlovaConfig, RequestOptions } from './type';
export const createAlovaRequest = <
RequestConfig = FetchRequestInit,
ResponseType = Response,
ResponseHeader = Headers,
L1Cache extends AlovaGlobalCacheAdapter = AlovaDefaultCacheAdapter,
L2Cache extends AlovaGlobalCacheAdapter = AlovaDefaultCacheAdapter
>(
customConfig: CustomAlovaConfig<
AlovaGenerics<any, any, RequestConfig, ResponseType, ResponseHeader, L1Cache, L2Cache, any>
>,
options: RequestOptions<AlovaGenerics<any, any, RequestConfig, ResponseType, ResponseHeader, L1Cache, L2Cache, any>>
) => {
const { tokenRefresher } = options;
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<
VueHookType,
AlovaRequestAdapter<RequestConfig, ResponseType, ResponseHeader>
>({
refreshTokenOnSuccess: {
isExpired: (response, method) => tokenRefresher?.isExpired(response, method) || false,
handler: async (response, method) => tokenRefresher?.handler(response, method)
},
refreshTokenOnError: {
isExpired: (response, method) => tokenRefresher?.isExpired(response, method) || false,
handler: async (response, method) => tokenRefresher?.handler(response, method)
}
});
const instance = createAlova({
...customConfig,
timeout: customConfig.timeout ?? 10 * 1000,
requestAdapter: (customConfig.requestAdapter as any) ?? adapterFetch(),
statesHook: VueHook,
beforeRequest: onAuthRequired(options.onRequest as any),
responded: onResponseRefreshToken({
onSuccess: async (response, method) => {
// check if http status is success
let error: any = null;
let transformedData: any = null;
try {
if (await options.isBackendSuccess(response)) {
transformedData = await options.transformBackendResponse(response);
} else {
error = new Error('the backend request error');
error.code = BACKEND_ERROR_CODE;
}
} catch (err) {
error = err;
}
if (error) {
await options.onError?.(error, response, method);
throw error;
}
return transformedData;
},
onComplete: options.onComplete,
onError: (error, method) => options.onError?.(error, null, method)
})
});
return instance;
};
export { BACKEND_ERROR_CODE };
export type * from './type';
export type * from 'alova';

View File

@@ -0,0 +1,52 @@
import type { AlovaGenerics, AlovaOptions, AlovaRequestAdapter, Method, ResponseCompleteHandler } from 'alova';
export type CustomAlovaConfig<AG extends AlovaGenerics> = Omit<
AlovaOptions<AG>,
'statesHook' | 'beforeRequest' | 'responded' | 'requestAdapter'
> & {
/** request adapter. all request of alova will be sent by it. */
requestAdapter?: AlovaRequestAdapter<AG['RequestConfig'], AG['Response'], AG['ResponseHeader']>;
};
export interface RequestOptions<AG extends AlovaGenerics> {
/**
* The hook before request
*
* For example: You can add header token in this hook
*
* @param method alova Method Instance
*/
onRequest?: AlovaOptions<AG>['beforeRequest'];
/**
* The hook to check backend response is success or not
*
* @param response alova response
*/
isBackendSuccess: (response: AG['Response']) => Promise<boolean>;
/** The config to refresh token */
tokenRefresher?: {
/** detect the token is expired */
isExpired(response: AG['Response'], Method: Method<AG>): Promise<boolean> | boolean;
/** refresh token handler */
handler(response: AG['Response'], Method: Method<AG>): Promise<void>;
};
/** The hook after backend request complete */
onComplete?: ResponseCompleteHandler<AG>;
/**
* The hook to handle error
*
* For example: You can show error message in this hook
*
* @param error
*/
onError?: (error: any, response: AG['Response'] | null, methodInstance: Method<AG>) => any | Promise<any>;
/**
* transform backend response when the responseType is json
*
* @param response alova response
*/
transformBackendResponse: (response: AG['Response']) => any;
}

View File

@@ -1,19 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"target": "ESNext",
"lib": ["DOM", "ESNext"],
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"jsx": "preserve",
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"types": ["node"]
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]

View File

@@ -0,0 +1,21 @@
{
"name": "@sa/axios",
"version": "1.3.7",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/utils": "workspace:*",
"axios": "1.7.7",
"axios-retry": "4.5.0",
"qs": "6.13.0"
},
"devDependencies": {
"@types/qs": "6.9.16"
}
}

View File

@@ -0,0 +1,5 @@
/** request id key */
export const REQUEST_ID_KEY = 'X-Request-Id';
/** the backend error code key */
export const BACKEND_ERROR_CODE = 'BACKEND_ERROR';

183
packages/axios/src/index.ts Normal file
View File

@@ -0,0 +1,183 @@
import axios, { AxiosError } from 'axios';
import type { AxiosResponse, CreateAxiosDefaults, InternalAxiosRequestConfig } from 'axios';
import axiosRetry from 'axios-retry';
import { nanoid } from '@sa/utils';
import { createAxiosConfig, createDefaultOptions, createRetryOptions } from './options';
import { BACKEND_ERROR_CODE, REQUEST_ID_KEY } from './constant';
import type {
CustomAxiosRequestConfig,
FlatRequestInstance,
MappedType,
RequestInstance,
RequestOption,
ResponseType
} from './type';
function createCommonRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const opts = createDefaultOptions<ResponseData>(options);
const axiosConf = createAxiosConfig(axiosConfig);
const instance = axios.create(axiosConf);
const abortControllerMap = new Map<string, AbortController>();
// config axios retry
const retryOptions = createRetryOptions(axiosConf);
axiosRetry(instance, retryOptions);
instance.interceptors.request.use(conf => {
const config: InternalAxiosRequestConfig = { ...conf };
// set request id
const requestId = nanoid();
config.headers.set(REQUEST_ID_KEY, requestId);
// config abort controller
if (!config.signal) {
const abortController = new AbortController();
config.signal = abortController.signal;
abortControllerMap.set(requestId, abortController);
}
// handle config by hook
const handledConfig = opts.onRequest?.(config) || config;
return handledConfig;
});
instance.interceptors.response.use(
async response => {
const responseType: ResponseType = (response.config?.responseType as ResponseType) || 'json';
if (responseType !== 'json' || opts.isBackendSuccess(response)) {
return Promise.resolve(response);
}
const fail = await opts.onBackendFail(response, instance);
if (fail) {
return fail;
}
const backendError = new AxiosError<ResponseData>(
'the backend request error',
BACKEND_ERROR_CODE,
response.config,
response.request,
response
);
await opts.onError(backendError);
return Promise.reject(backendError);
},
async (error: AxiosError<ResponseData>) => {
await opts.onError(error);
return Promise.reject(error);
}
);
function cancelRequest(requestId: string) {
const abortController = abortControllerMap.get(requestId);
if (abortController) {
abortController.abort();
abortControllerMap.delete(requestId);
}
}
function cancelAllRequest() {
abortControllerMap.forEach(abortController => {
abortController.abort();
});
abortControllerMap.clear();
}
return {
instance,
opts,
cancelRequest,
cancelAllRequest
};
}
/**
* create a request instance
*
* @param axiosConfig axios config
* @param options request options
*/
export function createRequest<ResponseData = any, State = Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
const request: RequestInstance<State> = async function request<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
const response: AxiosResponse<ResponseData> = await instance(config);
const responseType = response.config?.responseType || 'json';
if (responseType === 'json') {
return opts.transformBackendResponse(response);
}
return response.data as MappedType<R, T>;
} as RequestInstance<State>;
request.cancelRequest = cancelRequest;
request.cancelAllRequest = cancelAllRequest;
request.state = {} as State;
return request;
}
/**
* create a flat request instance
*
* The response data is a flat object: { data: any, error: AxiosError }
*
* @param axiosConfig axios config
* @param options request options
*/
export function createFlatRequest<ResponseData = any, State = Record<string, unknown>>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const { instance, opts, cancelRequest, cancelAllRequest } = createCommonRequest<ResponseData>(axiosConfig, options);
const flatRequest: FlatRequestInstance<State, ResponseData> = async function flatRequest<
T = any,
R extends ResponseType = 'json'
>(config: CustomAxiosRequestConfig) {
try {
const response: AxiosResponse<ResponseData> = await instance(config);
const responseType = response.config?.responseType || 'json';
if (responseType === 'json') {
const data = opts.transformBackendResponse(response);
return { data, error: null, response };
}
return { data: response.data as MappedType<R, T>, error: null };
} catch (error) {
return { data: null, error, response: (error as AxiosError<ResponseData>).response };
}
} as FlatRequestInstance<State, ResponseData>;
flatRequest.cancelRequest = cancelRequest;
flatRequest.cancelAllRequest = cancelAllRequest;
flatRequest.state = {} as State;
return flatRequest;
}
export { BACKEND_ERROR_CODE, REQUEST_ID_KEY };
export type * from './type';
export type { CreateAxiosDefaults, AxiosError };

View File

@@ -0,0 +1,48 @@
import type { CreateAxiosDefaults } from 'axios';
import type { IAxiosRetryConfig } from 'axios-retry';
import { stringify } from 'qs';
import { isHttpSuccess } from './shared';
import type { RequestOption } from './type';
export function createDefaultOptions<ResponseData = any>(options?: Partial<RequestOption<ResponseData>>) {
const opts: RequestOption<ResponseData> = {
onRequest: async config => config,
isBackendSuccess: _response => true,
onBackendFail: async () => {},
transformBackendResponse: async response => response.data,
onError: async () => {}
};
Object.assign(opts, options);
return opts;
}
export function createRetryOptions(config?: Partial<CreateAxiosDefaults>) {
const retryConfig: IAxiosRetryConfig = {
retries: 0
};
Object.assign(retryConfig, config);
return retryConfig;
}
export function createAxiosConfig(config?: Partial<CreateAxiosDefaults>) {
const TEN_SECONDS = 10 * 1000;
const axiosConfig: CreateAxiosDefaults = {
timeout: TEN_SECONDS,
headers: {
'Content-Type': 'application/json'
},
validateStatus: isHttpSuccess,
paramsSerializer: params => {
return stringify(params);
}
};
Object.assign(axiosConfig, config);
return axiosConfig;
}

View File

@@ -0,0 +1,28 @@
import type { AxiosHeaderValue, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
export function getContentType(config: InternalAxiosRequestConfig) {
const contentType: AxiosHeaderValue = config.headers?.['Content-Type'] || 'application/json';
return contentType;
}
/**
* check if http status is success
*
* @param status
*/
export function isHttpSuccess(status: number) {
const isSuccessCode = status >= 200 && status < 300;
return isSuccessCode || status === 304;
}
/**
* is response json
*
* @param response axios response
*/
export function isResponseJson(response: AxiosResponse) {
const { responseType } = response.config;
return responseType === 'json' || responseType === undefined;
}

115
packages/axios/src/type.ts Normal file
View File

@@ -0,0 +1,115 @@
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
export type ContentType =
| 'text/html'
| 'text/plain'
| 'multipart/form-data'
| 'application/json'
| 'application/x-www-form-urlencoded'
| 'application/octet-stream';
export interface RequestOption<ResponseData = any> {
/**
* The hook before request
*
* For example: You can add header token in this hook
*
* @param config Axios config
*/
onRequest: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
/**
* The hook to check backend response is success or not
*
* @param response Axios response
*/
isBackendSuccess: (response: AxiosResponse<ResponseData>) => boolean;
/**
* The hook after backend request fail
*
* For example: You can handle the expired token in this hook
*
* @param response Axios response
* @param instance Axios instance
*/
onBackendFail: (
response: AxiosResponse<ResponseData>,
instance: AxiosInstance
) => Promise<AxiosResponse | null> | Promise<void>;
/**
* transform backend response when the responseType is json
*
* @param response Axios response
*/
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
/**
* The hook to handle error
*
* For example: You can show error message in this hook
*
* @param error
*/
onError: (error: AxiosError<ResponseData>) => void | Promise<void>;
}
interface ResponseMap {
blob: Blob;
text: string;
arrayBuffer: ArrayBuffer;
stream: ReadableStream<Uint8Array>;
document: Document;
}
export type ResponseType = keyof ResponseMap | 'json';
export type MappedType<R extends ResponseType, JsonType = any> = R extends keyof ResponseMap
? ResponseMap[R]
: JsonType;
export type CustomAxiosRequestConfig<R extends ResponseType = 'json'> = Omit<AxiosRequestConfig, 'responseType'> & {
responseType?: R;
};
export interface RequestInstanceCommon<T> {
/**
* cancel the request by request id
*
* if the request provide abort controller sign from config, it will not collect in the abort controller map
*
* @param requestId
*/
cancelRequest: (requestId: string) => void;
/**
* cancel all request
*
* if the request provide abort controller sign from config, it will not collect in the abort controller map
*/
cancelAllRequest: () => void;
/** you can set custom state in the request instance */
state: T;
}
/** The request instance */
export interface RequestInstance<S = Record<string, unknown>> extends RequestInstanceCommon<S> {
<T = any, R extends ResponseType = 'json'>(config: CustomAxiosRequestConfig<R>): Promise<MappedType<R, T>>;
}
export type FlatResponseSuccessData<T = any, ResponseData = any> = {
data: T;
error: null;
response: AxiosResponse<ResponseData>;
};
export type FlatResponseFailData<ResponseData = any> = {
data: null;
error: AxiosError<ResponseData>;
response: AxiosResponse<ResponseData>;
};
export type FlatResponseData<T = any, ResponseData = any> =
| FlatResponseSuccessData<T, ResponseData>
| FlatResponseFailData<ResponseData>;
export interface FlatRequestInstance<S = Record<string, unknown>, ResponseData = any> extends RequestInstanceCommon<S> {
<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig<R>
): Promise<FlatResponseData<MappedType<R, T>, ResponseData>>;
}

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,29 +0,0 @@
import { colord, extend } from 'colord';
import type { HslColor } from 'colord';
import labPlugin from 'colord/plugins/lab';
extend([labPlugin]);
export function isValidColor(color: string) {
return colord(color).isValid();
}
export function getHex(color: string) {
return colord(color).toHex();
}
export function getRgb(color: string) {
return colord(color).toRgb();
}
export function getHsl(color: string) {
return colord(color).toHsl();
}
export function getDeltaE(color1: string, color2: string) {
return colord(color1).delta(color2);
}
export function transformHslToHex(color: HslColor) {
return colord(color).toHex();
}

View File

@@ -1,42 +0,0 @@
import { getColorPaletteFamily } from './palette';
import { getColorName } from './name';
import type { ColorPalette, ColorPaletteNumber, ColorPaletteItem, ColorPaletteFamily } from './type';
import defaultPalettes from './json/palette.json';
/**
* get color palette by provided color and color name
* @param color the provided color
* @param colorName color name
*/
export function getColorPalette(color: string, colorName: string) {
const colorPaletteFamily = getColorPaletteFamily(color, colorName);
const colorMap = new Map<ColorPaletteNumber, ColorPaletteItem>();
colorPaletteFamily.palettes.forEach(palette => {
colorMap.set(palette.number, palette);
});
const mainColor = colorMap.get(500) as ColorPaletteItem;
const matchColor = colorPaletteFamily.palettes.find(palette => palette.hexcode === color) as ColorPaletteItem;
const colorPalette: ColorPalette = {
...colorPaletteFamily,
colorMap,
main: mainColor,
match: matchColor
};
return colorPalette;
}
export default getColorPalette;
/**
* the builtin color palettes
*/
const colorPalettes = defaultPalettes as ColorPaletteFamily[];
export { getColorName, colorPalettes };
export type { ColorPalette, ColorPaletteNumber, ColorPaletteItem, ColorPaletteFamily };

File diff suppressed because it is too large Load Diff

View File

@@ -1,274 +0,0 @@
[
{
"key": "red",
"palettes": [
{ "hexcode": "#fef2f2", "number": 50, "name": "Bridesmaid" },
{ "hexcode": "#fee2e2", "number": 100, "name": "Pippin" },
{ "hexcode": "#fecaca", "number": 200, "name": "Your Pink" },
{ "hexcode": "#fca5a5", "number": 300, "name": "Cornflower Lilac" },
{ "hexcode": "#f87171", "number": 400, "name": "Bittersweet" },
{ "hexcode": "#ef4444", "number": 500, "name": "Cinnabar" },
{ "hexcode": "#dc2626", "number": 600, "name": "Persian Red" },
{ "hexcode": "#b91c1c", "number": 700, "name": "Thunderbird" },
{ "hexcode": "#991b1b", "number": 800, "name": "Old Brick" },
{ "hexcode": "#7f1d1d", "number": 900, "name": "Falu Red" },
{ "hexcode": "#450a0a", "number": 950, "name": "Mahogany" }
]
},
{
"key": "orange",
"palettes": [
{ "hexcode": "#fff7ed", "number": 50, "name": "Serenade" },
{ "hexcode": "#ffedd5", "number": 100, "name": "Derby" },
{ "hexcode": "#fed7aa", "number": 200, "name": "Caramel" },
{ "hexcode": "#fdba74", "number": 300, "name": "Macaroni and Cheese" },
{ "hexcode": "#fb923c", "number": 400, "name": "Neon Carrot" },
{ "hexcode": "#f97316", "number": 500, "name": "Ecstasy" },
{ "hexcode": "#ea580c", "number": 600, "name": "Trinidad" },
{ "hexcode": "#c2410c", "number": 700, "name": "Tia Maria" },
{ "hexcode": "#9a3412", "number": 800, "name": "Tabasco" },
{ "hexcode": "#7c2d12", "number": 900, "name": "Pueblo" },
{ "hexcode": "#431407", "number": 950, "name": "Rebel" }
]
},
{
"key": "amber",
"palettes": [
{ "hexcode": "#fffbeb", "number": 50, "name": "Island Spice" },
{ "hexcode": "#fef3c7", "number": 100, "name": "Beeswax" },
{ "hexcode": "#fde68a", "number": 200, "name": "Sweet Corn" },
{ "hexcode": "#fcd34d", "number": 300, "name": "Mustard" },
{ "hexcode": "#fbbf24", "number": 400, "name": "Lightning Yellow" },
{ "hexcode": "#f59e0b", "number": 500, "name": "California" },
{ "hexcode": "#d97706", "number": 600, "name": "Christine" },
{ "hexcode": "#b45309", "number": 700, "name": "Vesuvius" },
{ "hexcode": "#92400e", "number": 800, "name": "Korma" },
{ "hexcode": "#78350f", "number": 900, "name": "Copper Canyon" },
{ "hexcode": "#451a03", "number": 950, "name": "Brown Pod" }
]
},
{
"key": "yellow",
"palettes": [
{ "hexcode": "#fefce8", "number": 50, "name": "Orange White" },
{ "hexcode": "#fef9c3", "number": 100, "name": "Lemon Chiffon" },
{ "hexcode": "#fef08a", "number": 200, "name": "Sweet Corn" },
{ "hexcode": "#fde047", "number": 300, "name": "Bright Sun" },
{ "hexcode": "#facc15", "number": 400, "name": "Candlelight" },
{ "hexcode": "#eab308", "number": 500, "name": "Corn" },
{ "hexcode": "#ca8a04", "number": 600, "name": "Pirate Gold" },
{ "hexcode": "#a16207", "number": 700, "name": "Mai Tai" },
{ "hexcode": "#854d0e", "number": 800, "name": "Korma" },
{ "hexcode": "#713f12", "number": 900, "name": "Sepia" },
{ "hexcode": "#422006", "number": 950, "name": "Dark Ebony" }
]
},
{
"key": "lime",
"palettes": [
{ "hexcode": "#f7fee7", "number": 50, "name": "Spring Sun" },
{ "hexcode": "#ecfccb", "number": 100, "name": "Chiffon" },
{ "hexcode": "#d9f99d", "number": 200, "name": "Gossip" },
{ "hexcode": "#bef264", "number": 300, "name": "Sulu" },
{ "hexcode": "#a3e635", "number": 400, "name": "Conifer" },
{ "hexcode": "#84cc16", "number": 500, "name": "Lima" },
{ "hexcode": "#65a30d", "number": 600, "name": "Christi" },
{ "hexcode": "#4d7c0f", "number": 700, "name": "Green Leaf" },
{ "hexcode": "#3f6212", "number": 800, "name": "Dell" },
{ "hexcode": "#365314", "number": 900, "name": "Clover" },
{ "hexcode": "#1a2e05", "number": 950, "name": "Deep Forest Green" }
]
},
{
"key": "green",
"palettes": [
{ "hexcode": "#f0fdf4", "number": 50, "name": "Ottoman" },
{ "hexcode": "#dcfce7", "number": 100, "name": "Blue Romance" },
{ "hexcode": "#bbf7d0", "number": 200, "name": "Magic Mint" },
{ "hexcode": "#86efac", "number": 300, "name": "Algae Green" },
{ "hexcode": "#4ade80", "number": 400, "name": "Emerald" },
{ "hexcode": "#22c55e", "number": 500, "name": "Malachite" },
{ "hexcode": "#16a34a", "number": 600, "name": "Salem" },
{ "hexcode": "#15803d", "number": 700, "name": "Jewel" },
{ "hexcode": "#166534", "number": 800, "name": "Jewel" },
{ "hexcode": "#14532d", "number": 900, "name": "Green Pea" },
{ "hexcode": "#052e16", "number": 950, "name": "English Holly" }
]
},
{
"key": "emerald",
"palettes": [
{ "hexcode": "#ecfdf5", "number": 50, "name": "White Ice" },
{ "hexcode": "#d1fae5", "number": 100, "name": "Granny Apple" },
{ "hexcode": "#a7f3d0", "number": 200, "name": "Magic Mint" },
{ "hexcode": "#6ee7b7", "number": 300, "name": "Bermuda" },
{ "hexcode": "#34d399", "number": 400, "name": "Shamrock" },
{ "hexcode": "#10b981", "number": 500, "name": "Mountain Meadow" },
{ "hexcode": "#059669", "number": 600, "name": "Green Haze" },
{ "hexcode": "#047857", "number": 700, "name": "Watercourse" },
{ "hexcode": "#065f46", "number": 800, "name": "Watercourse" },
{ "hexcode": "#064e3b", "number": 900, "name": "Evening Sea" },
{ "hexcode": "#022c22", "number": 950, "name": "Burnham" }
]
},
{
"key": "teal",
"palettes": [
{ "hexcode": "#f0fdfa", "number": 50, "name": "White Ice" },
{ "hexcode": "#ccfbf1", "number": 100, "name": "Scandal" },
{ "hexcode": "#99f6e4", "number": 200, "name": "Ice Cold" },
{ "hexcode": "#5eead4", "number": 300, "name": "Turquoise Blue" },
{ "hexcode": "#2dd4bf", "number": 400, "name": "Turquoise" },
{ "hexcode": "#14b8a6", "number": 500, "name": "Java" },
{ "hexcode": "#0d9488", "number": 600, "name": "Blue Chill" },
{ "hexcode": "#0f766e", "number": 700, "name": "Genoa" },
{ "hexcode": "#115e59", "number": 800, "name": "Eden" },
{ "hexcode": "#134e4a", "number": 900, "name": "Eden" },
{ "hexcode": "#042f2e", "number": 950, "name": "Tiber" }
]
},
{
"key": "cyan",
"palettes": [
{ "hexcode": "#ecfeff", "number": 50, "name": "Bubbles" },
{ "hexcode": "#cffafe", "number": 100, "name": "Oyster Bay" },
{ "hexcode": "#a5f3fc", "number": 200, "name": "Anakiwa" },
{ "hexcode": "#67e8f9", "number": 300, "name": "Spray" },
{ "hexcode": "#22d3ee", "number": 400, "name": "Bright Turquoise" },
{ "hexcode": "#06b6d4", "number": 500, "name": "Cerulean" },
{ "hexcode": "#0891b2", "number": 600, "name": "Bondi Blue" },
{ "hexcode": "#0e7490", "number": 700, "name": "Blue Chill" },
{ "hexcode": "#155e75", "number": 800, "name": "Blumine" },
{ "hexcode": "#164e63", "number": 900, "name": "Chathams Blue" },
{ "hexcode": "#083344", "number": 950, "name": "Tarawera" }
]
},
{
"key": "sky",
"palettes": [
{ "hexcode": "#f0f9ff", "number": 50, "name": "Alice Blue" },
{ "hexcode": "#e0f2fe", "number": 100, "name": "Pattens Blue" },
{ "hexcode": "#bae6fd", "number": 200, "name": "French Pass" },
{ "hexcode": "#7dd3fc", "number": 300, "name": "Malibu" },
{ "hexcode": "#38bdf8", "number": 400, "name": "Picton Blue" },
{ "hexcode": "#0ea5e9", "number": 500, "name": "Cerulean" },
{ "hexcode": "#0284c7", "number": 600, "name": "Lochmara" },
{ "hexcode": "#0369a1", "number": 700, "name": "Bahama Blue" },
{ "hexcode": "#075985", "number": 800, "name": "Venice Blue" },
{ "hexcode": "#0c4a6e", "number": 900, "name": "Chathams Blue" },
{ "hexcode": "#082f49", "number": 950, "name": "Blue Whale" }
]
},
{
"key": "blue",
"palettes": [
{ "hexcode": "#eff6ff", "number": 50, "name": "Zumthor" },
{ "hexcode": "#dbeafe", "number": 100, "name": "Hawkes Blue" },
{ "hexcode": "#bfdbfe", "number": 200, "name": "Tropical Blue" },
{ "hexcode": "#93c5fd", "number": 300, "name": "Malibu" },
{ "hexcode": "#60a5fa", "number": 400, "name": "Cornflower Blue" },
{ "hexcode": "#3b82f6", "number": 500, "name": "Dodger Blue" },
{ "hexcode": "#2563eb", "number": 600, "name": "Royal Blue" },
{ "hexcode": "#1d4ed8", "number": 700, "name": "Cerulean Blue" },
{ "hexcode": "#1e40af", "number": 800, "name": "Persian Blue" },
{ "hexcode": "#1e3a8a", "number": 900, "name": "Bay of Many" },
{ "hexcode": "#172554", "number": 950, "name": "Bunting" }
]
},
{
"key": "indigo",
"palettes": [
{ "hexcode": "#eef2ff", "number": 50, "name": "Zircon" },
{ "hexcode": "#e0e7ff", "number": 100, "name": "Hawkes Blue" },
{ "hexcode": "#c7d2fe", "number": 200, "name": "Periwinkle" },
{ "hexcode": "#a5b4fc", "number": 300, "name": "Perano" },
{ "hexcode": "#818cf8", "number": 400, "name": "Portage" },
{ "hexcode": "#6366f1", "number": 500, "name": "Royal Blue" },
{ "hexcode": "#4f46e5", "number": 600, "name": "Royal Blue" },
{ "hexcode": "#4338ca", "number": 700, "name": "Governor Bay" },
{ "hexcode": "#3730a3", "number": 800, "name": "Governor Bay" },
{ "hexcode": "#312e81", "number": 900, "name": "Minsk" },
{ "hexcode": "#1e1b4b", "number": 950, "name": "Port Gore" }
]
},
{
"key": "violet",
"palettes": [
{ "hexcode": "#f5f3ff", "number": 50, "name": "Titan White" },
{ "hexcode": "#ede9fe", "number": 100, "name": "Titan White" },
{ "hexcode": "#ddd6fe", "number": 200, "name": "Fog" },
{ "hexcode": "#c4b5fd", "number": 300, "name": "Melrose" },
{ "hexcode": "#a78bfa", "number": 400, "name": "Dull Lavender" },
{ "hexcode": "#8b5cf6", "number": 500, "name": "Medium Purple" },
{ "hexcode": "#7c3aed", "number": 600, "name": "Purple Heart" },
{ "hexcode": "#6d28d9", "number": 700, "name": "Purple Heart" },
{ "hexcode": "#5b21b6", "number": 800, "name": "Purple Heart" },
{ "hexcode": "#4c1d95", "number": 900, "name": "Daisy Bush" },
{ "hexcode": "#2e1065", "number": 950, "name": "Violent Violet" }
]
},
{
"key": "purple",
"palettes": [
{ "hexcode": "#faf5ff", "number": 50, "name": "Magnolia" },
{ "hexcode": "#f3e8ff", "number": 100, "name": "Blue Chalk" },
{ "hexcode": "#e9d5ff", "number": 200, "name": "Blue Chalk" },
{ "hexcode": "#d8b4fe", "number": 300, "name": "Mauve" },
{ "hexcode": "#c084fc", "number": 400, "name": "Heliotrope" },
{ "hexcode": "#a855f7", "number": 500, "name": "Medium Purple" },
{ "hexcode": "#9333ea", "number": 600, "name": "Electric Violet" },
{ "hexcode": "#7e22ce", "number": 700, "name": "Purple Heart" },
{ "hexcode": "#6b21a8", "number": 800, "name": "Seance" },
{ "hexcode": "#581c87", "number": 900, "name": "Daisy Bush" },
{ "hexcode": "#3b0764", "number": 950, "name": "Christalle" }
]
},
{
"key": "fuchsia",
"palettes": [
{ "hexcode": "#fdf4ff", "number": 50, "name": "White Pointer" },
{ "hexcode": "#fae8ff", "number": 100, "name": "White Pointer" },
{ "hexcode": "#f5d0fe", "number": 200, "name": "Mauve" },
{ "hexcode": "#f0abfc", "number": 300, "name": "Mauve" },
{ "hexcode": "#e879f9", "number": 400, "name": "Heliotrope" },
{ "hexcode": "#d946ef", "number": 500, "name": "Heliotrope" },
{ "hexcode": "#c026d3", "number": 600, "name": "Fuchsia Pink" },
{ "hexcode": "#a21caf", "number": 700, "name": "Violet Eggplant" },
{ "hexcode": "#86198f", "number": 800, "name": "Seance" },
{ "hexcode": "#701a75", "number": 900, "name": "Seance" },
{ "hexcode": "#4a044e", "number": 950, "name": "Clairvoyant" }
]
},
{
"key": "pink",
"palettes": [
{ "hexcode": "#fdf2f8", "number": 50, "name": "Wisp Pink" },
{ "hexcode": "#fce7f3", "number": 100, "name": "Carousel Pink" },
{ "hexcode": "#fbcfe8", "number": 200, "name": "Classic Rose" },
{ "hexcode": "#f9a8d4", "number": 300, "name": "Lavender Pink" },
{ "hexcode": "#f472b6", "number": 400, "name": "Persian Pink" },
{ "hexcode": "#ec4899", "number": 500, "name": "Brilliant Rose" },
{ "hexcode": "#db2777", "number": 600, "name": "Cerise" },
{ "hexcode": "#be185d", "number": 700, "name": "Maroon Flush" },
{ "hexcode": "#9d174d", "number": 800, "name": "Disco" },
{ "hexcode": "#831843", "number": 900, "name": "Disco" },
{ "hexcode": "#500724", "number": 950, "name": "Cab Sav" }
]
},
{
"key": "rose",
"palettes": [
{ "hexcode": "#fff1f2", "number": 50, "name": "Lavender blush" },
{ "hexcode": "#ffe4e6", "number": 100, "name": "Cosmos" },
{ "hexcode": "#fecdd3", "number": 200, "name": "Pastel Pink" },
{ "hexcode": "#fda4af", "number": 300, "name": "Sweet Pink" },
{ "hexcode": "#fb7185", "number": 400, "name": "Froly" },
{ "hexcode": "#f43f5e", "number": 500, "name": "Radical Red" },
{ "hexcode": "#e11d48", "number": 600, "name": "Amaranth" },
{ "hexcode": "#be123c", "number": 700, "name": "Cardinal" },
{ "hexcode": "#9f1239", "number": 800, "name": "Shiraz" },
{ "hexcode": "#881337", "number": 900, "name": "Claret" },
{ "hexcode": "#4c0519", "number": 950, "name": "Cab Sav" }
]
}
]

View File

@@ -1,95 +0,0 @@
import { isValidColor, getHsl, getDeltaE, transformHslToHex } from './color';
import { getColorName } from './name';
import type { ColorPaletteFamily, ColorPaletteFamilyWithNearestPalette } from './type';
import defaultPalettes from './json/palette.json';
export function getNearestColorPaletteFamily(color: string, families: ColorPaletteFamily[]) {
const familyWithConfig = families.map(family => {
const palettes = family.palettes.map(palette => {
return {
...palette,
delta: getDeltaE(color, palette.hexcode)
};
});
const nearestPalette = palettes.reduce((prev, curr) => (prev.delta < curr.delta ? prev : curr));
return {
...family,
palettes,
nearestPalette
};
});
const nearestPaletteFamily = familyWithConfig.reduce((prev, curr) =>
prev.nearestPalette.delta < curr.nearestPalette.delta ? prev : curr
);
const { l } = getHsl(color);
const paletteFamily: ColorPaletteFamilyWithNearestPalette = {
...nearestPaletteFamily,
nearestLightnessPalette: nearestPaletteFamily.palettes.reduce((prev, curr) => {
const { l: prevLightness } = getHsl(prev.hexcode);
const { l: currLightness } = getHsl(curr.hexcode);
const deltaPrev = Math.abs(prevLightness - l);
const deltaCurr = Math.abs(currLightness - l);
return deltaPrev < deltaCurr ? prev : curr;
})
};
return paletteFamily;
}
export function getColorPaletteFamily(color: string, colorName: string) {
if (!isValidColor(color)) {
throw new Error('Invalid color, please check color value!');
}
const { h: h1, s: s1 } = getHsl(color);
const { nearestLightnessPalette, palettes } = getNearestColorPaletteFamily(
color,
defaultPalettes as ColorPaletteFamily[]
);
const { number, hexcode } = nearestLightnessPalette;
const { h: h2, s: s2 } = getHsl(hexcode);
const deltaH = h1 - h2 || h2;
const sRatio = s1 / s2;
const colorPaletteFamily: ColorPaletteFamily = {
key: colorName,
palettes: palettes.map(palette => {
let hexValue = color;
const isSame = number === palette.number;
if (!isSame) {
const { h: h3, s: s3, l } = getHsl(palette.hexcode);
const newH = deltaH < 0 ? h3 + deltaH : deltaH;
const newS = s3 * sRatio;
hexValue = transformHslToHex({
h: newH,
s: newS,
l
});
}
return {
hexcode: hexValue,
number: palette.number,
name: getColorName(hexValue)
};
})
};
return colorPaletteFamily;
}

View File

@@ -1,63 +0,0 @@
/**
* the color palette number
*/
export type ColorPaletteNumber = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
/**
* the color palette item
*/
export type ColorPaletteItem = {
/**
* the color hexcode
*/
hexcode: string;
/**
* the color number
* @link {@link ColorPaletteNumber}
*/
number: ColorPaletteNumber;
/**
* the color name
*/
name: string;
};
export type ColorPaletteFamily = {
/**
* the color palette family key
*/
key: string;
/**
* the color palette family's palettes
*/
palettes: ColorPaletteItem[];
};
export type ColorPaletteWithDelta = ColorPaletteItem & {
delta: number;
};
export type ColorPaletteItemWithName = ColorPaletteItem & {
name: string;
};
export type ColorPaletteFamilyWithNearestPalette = ColorPaletteFamily & {
nearestPalette: ColorPaletteWithDelta;
nearestLightnessPalette: ColorPaletteWithDelta;
};
export type ColorPalette = ColorPaletteFamily & {
/**
* the color map of the palette
*/
colorMap: Map<ColorPaletteNumber, ColorPaletteItem>;
/**
* the main color of the palette
* @description which number is 500
*/
main: ColorPaletteItemWithName;
/**
* the match color of the palette
*/
match: ColorPaletteItemWithName;
};

View File

@@ -1,17 +1,16 @@
{
"name": "@sa/color-palette",
"version": "1.0.0",
"name": "@sa/color",
"version": "1.3.7",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": [
"./src/*"
]
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/utils": "workspace:*",
"colord": "2.9.3"
}
}

View File

@@ -0,0 +1,2 @@
export * from './name';
export * from './palette';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,356 @@
import type { ColorPaletteFamily } from '../types';
export const colorPalettes: ColorPaletteFamily[] = [
{
name: 'Slate',
palettes: [
{ hex: '#f8fafc', number: 50 },
{ hex: '#f1f5f9', number: 100 },
{ hex: '#e2e8f0', number: 200 },
{ hex: '#cbd5e1', number: 300 },
{ hex: '#94a3b8', number: 400 },
{ hex: '#64748b', number: 500 },
{ hex: '#475569', number: 600 },
{ hex: '#334155', number: 700 },
{ hex: '#1e293b', number: 800 },
{ hex: '#0f172a', number: 900 },
{ hex: '#020617', number: 950 }
]
},
{
name: 'Gray',
palettes: [
{ hex: '#f9fafb', number: 50 },
{ hex: '#f3f4f6', number: 100 },
{ hex: '#e5e7eb', number: 200 },
{ hex: '#d1d5db', number: 300 },
{ hex: '#9ca3af', number: 400 },
{ hex: '#6b7280', number: 500 },
{ hex: '#4b5563', number: 600 },
{ hex: '#374151', number: 700 },
{ hex: '#1f2937', number: 800 },
{ hex: '#111827', number: 900 },
{ hex: '#030712', number: 950 }
]
},
{
name: 'Zinc',
palettes: [
{ hex: '#fafafa', number: 50 },
{ hex: '#f4f4f5', number: 100 },
{ hex: '#e4e4e7', number: 200 },
{ hex: '#d4d4d8', number: 300 },
{ hex: '#a1a1aa', number: 400 },
{ hex: '#71717a', number: 500 },
{ hex: '#52525b', number: 600 },
{ hex: '#3f3f46', number: 700 },
{ hex: '#27272a', number: 800 },
{ hex: '#18181b', number: 900 },
{ hex: '#09090b', number: 950 }
]
},
{
name: 'Neutral',
palettes: [
{ hex: '#fafafa', number: 50 },
{ hex: '#f5f5f5', number: 100 },
{ hex: '#e5e5e5', number: 200 },
{ hex: '#d4d4d4', number: 300 },
{ hex: '#a3a3a3', number: 400 },
{ hex: '#737373', number: 500 },
{ hex: '#525252', number: 600 },
{ hex: '#404040', number: 700 },
{ hex: '#262626', number: 800 },
{ hex: '#171717', number: 900 },
{ hex: '#0a0a0a', number: 950 }
]
},
{
name: 'Stone',
palettes: [
{ hex: '#fafaf9', number: 50 },
{ hex: '#f5f5f4', number: 100 },
{ hex: '#e7e5e4', number: 200 },
{ hex: '#d6d3d1', number: 300 },
{ hex: '#a8a29e', number: 400 },
{ hex: '#78716c', number: 500 },
{ hex: '#57534e', number: 600 },
{ hex: '#44403c', number: 700 },
{ hex: '#292524', number: 800 },
{ hex: '#1c1917', number: 900 },
{ hex: '#0c0a09', number: 950 }
]
},
{
name: 'Red',
palettes: [
{ hex: '#fef2f2', number: 50 },
{ hex: '#fee2e2', number: 100 },
{ hex: '#fecaca', number: 200 },
{ hex: '#fca5a5', number: 300 },
{ hex: '#f87171', number: 400 },
{ hex: '#ef4444', number: 500 },
{ hex: '#dc2626', number: 600 },
{ hex: '#b91c1c', number: 700 },
{ hex: '#991b1b', number: 800 },
{ hex: '#7f1d1d', number: 900 },
{ hex: '#450a0a', number: 950 }
]
},
{
name: 'Orange',
palettes: [
{ hex: '#fff7ed', number: 50 },
{ hex: '#ffedd5', number: 100 },
{ hex: '#fed7aa', number: 200 },
{ hex: '#fdba74', number: 300 },
{ hex: '#fb923c', number: 400 },
{ hex: '#f97316', number: 500 },
{ hex: '#ea580c', number: 600 },
{ hex: '#c2410c', number: 700 },
{ hex: '#9a3412', number: 800 },
{ hex: '#7c2d12', number: 900 },
{ hex: '#431407', number: 950 }
]
},
{
name: 'Amber',
palettes: [
{ hex: '#fffbeb', number: 50 },
{ hex: '#fef3c7', number: 100 },
{ hex: '#fde68a', number: 200 },
{ hex: '#fcd34d', number: 300 },
{ hex: '#fbbf24', number: 400 },
{ hex: '#f59e0b', number: 500 },
{ hex: '#d97706', number: 600 },
{ hex: '#b45309', number: 700 },
{ hex: '#92400e', number: 800 },
{ hex: '#78350f', number: 900 },
{ hex: '#451a03', number: 950 }
]
},
{
name: 'Yellow',
palettes: [
{ hex: '#fefce8', number: 50 },
{ hex: '#fef9c3', number: 100 },
{ hex: '#fef08a', number: 200 },
{ hex: '#fde047', number: 300 },
{ hex: '#facc15', number: 400 },
{ hex: '#eab308', number: 500 },
{ hex: '#ca8a04', number: 600 },
{ hex: '#a16207', number: 700 },
{ hex: '#854d0e', number: 800 },
{ hex: '#713f12', number: 900 },
{ hex: '#422006', number: 950 }
]
},
{
name: 'Lime',
palettes: [
{ hex: '#f7fee7', number: 50 },
{ hex: '#ecfccb', number: 100 },
{ hex: '#d9f99d', number: 200 },
{ hex: '#bef264', number: 300 },
{ hex: '#a3e635', number: 400 },
{ hex: '#84cc16', number: 500 },
{ hex: '#65a30d', number: 600 },
{ hex: '#4d7c0f', number: 700 },
{ hex: '#3f6212', number: 800 },
{ hex: '#365314', number: 900 },
{ hex: '#1a2e05', number: 950 }
]
},
{
name: 'Green',
palettes: [
{ hex: '#f0fdf4', number: 50 },
{ hex: '#dcfce7', number: 100 },
{ hex: '#bbf7d0', number: 200 },
{ hex: '#86efac', number: 300 },
{ hex: '#4ade80', number: 400 },
{ hex: '#22c55e', number: 500 },
{ hex: '#16a34a', number: 600 },
{ hex: '#15803d', number: 700 },
{ hex: '#166534', number: 800 },
{ hex: '#14532d', number: 900 },
{ hex: '#052e16', number: 950 }
]
},
{
name: 'Emerald',
palettes: [
{ hex: '#ecfdf5', number: 50 },
{ hex: '#d1fae5', number: 100 },
{ hex: '#a7f3d0', number: 200 },
{ hex: '#6ee7b7', number: 300 },
{ hex: '#34d399', number: 400 },
{ hex: '#10b981', number: 500 },
{ hex: '#059669', number: 600 },
{ hex: '#047857', number: 700 },
{ hex: '#065f46', number: 800 },
{ hex: '#064e3b', number: 900 },
{ hex: '#022c22', number: 950 }
]
},
{
name: 'Teal',
palettes: [
{ hex: '#f0fdfa', number: 50 },
{ hex: '#ccfbf1', number: 100 },
{ hex: '#99f6e4', number: 200 },
{ hex: '#5eead4', number: 300 },
{ hex: '#2dd4bf', number: 400 },
{ hex: '#14b8a6', number: 500 },
{ hex: '#0d9488', number: 600 },
{ hex: '#0f766e', number: 700 },
{ hex: '#115e59', number: 800 },
{ hex: '#134e4a', number: 900 },
{ hex: '#042f2e', number: 950 }
]
},
{
name: 'Cyan',
palettes: [
{ hex: '#ecfeff', number: 50 },
{ hex: '#cffafe', number: 100 },
{ hex: '#a5f3fc', number: 200 },
{ hex: '#67e8f9', number: 300 },
{ hex: '#22d3ee', number: 400 },
{ hex: '#06b6d4', number: 500 },
{ hex: '#0891b2', number: 600 },
{ hex: '#0e7490', number: 700 },
{ hex: '#155e75', number: 800 },
{ hex: '#164e63', number: 900 },
{ hex: '#083344', number: 950 }
]
},
{
name: 'Sky',
palettes: [
{ hex: '#f0f9ff', number: 50 },
{ hex: '#e0f2fe', number: 100 },
{ hex: '#bae6fd', number: 200 },
{ hex: '#7dd3fc', number: 300 },
{ hex: '#38bdf8', number: 400 },
{ hex: '#0ea5e9', number: 500 },
{ hex: '#0284c7', number: 600 },
{ hex: '#0369a1', number: 700 },
{ hex: '#075985', number: 800 },
{ hex: '#0c4a6e', number: 900 },
{ hex: '#082f49', number: 950 }
]
},
{
name: 'Blue',
palettes: [
{ hex: '#eff6ff', number: 50 },
{ hex: '#dbeafe', number: 100 },
{ hex: '#bfdbfe', number: 200 },
{ hex: '#93c5fd', number: 300 },
{ hex: '#60a5fa', number: 400 },
{ hex: '#3b82f6', number: 500 },
{ hex: '#2563eb', number: 600 },
{ hex: '#1d4ed8', number: 700 },
{ hex: '#1e40af', number: 800 },
{ hex: '#1e3a8a', number: 900 },
{ hex: '#172554', number: 950 }
]
},
{
name: 'Indigo',
palettes: [
{ hex: '#eef2ff', number: 50 },
{ hex: '#e0e7ff', number: 100 },
{ hex: '#c7d2fe', number: 200 },
{ hex: '#a5b4fc', number: 300 },
{ hex: '#818cf8', number: 400 },
{ hex: '#6366f1', number: 500 },
{ hex: '#4f46e5', number: 600 },
{ hex: '#4338ca', number: 700 },
{ hex: '#3730a3', number: 800 },
{ hex: '#312e81', number: 900 },
{ hex: '#1e1b4b', number: 950 }
]
},
{
name: 'Violet',
palettes: [
{ hex: '#f5f3ff', number: 50 },
{ hex: '#ede9fe', number: 100 },
{ hex: '#ddd6fe', number: 200 },
{ hex: '#c4b5fd', number: 300 },
{ hex: '#a78bfa', number: 400 },
{ hex: '#8b5cf6', number: 500 },
{ hex: '#7c3aed', number: 600 },
{ hex: '#6d28d9', number: 700 },
{ hex: '#5b21b6', number: 800 },
{ hex: '#4c1d95', number: 900 },
{ hex: '#2e1065', number: 950 }
]
},
{
name: 'Purple',
palettes: [
{ hex: '#faf5ff', number: 50 },
{ hex: '#f3e8ff', number: 100 },
{ hex: '#e9d5ff', number: 200 },
{ hex: '#d8b4fe', number: 300 },
{ hex: '#c084fc', number: 400 },
{ hex: '#a855f7', number: 500 },
{ hex: '#9333ea', number: 600 },
{ hex: '#7e22ce', number: 700 },
{ hex: '#6b21a8', number: 800 },
{ hex: '#581c87', number: 900 },
{ hex: '#3b0764', number: 950 }
]
},
{
name: 'Fuchsia',
palettes: [
{ hex: '#fdf4ff', number: 50 },
{ hex: '#fae8ff', number: 100 },
{ hex: '#f5d0fe', number: 200 },
{ hex: '#f0abfc', number: 300 },
{ hex: '#e879f9', number: 400 },
{ hex: '#d946ef', number: 500 },
{ hex: '#c026d3', number: 600 },
{ hex: '#a21caf', number: 700 },
{ hex: '#86198f', number: 800 },
{ hex: '#701a75', number: 900 },
{ hex: '#4a044e', number: 950 }
]
},
{
name: 'Pink',
palettes: [
{ hex: '#fdf2f8', number: 50 },
{ hex: '#fce7f3', number: 100 },
{ hex: '#fbcfe8', number: 200 },
{ hex: '#f9a8d4', number: 300 },
{ hex: '#f472b6', number: 400 },
{ hex: '#ec4899', number: 500 },
{ hex: '#db2777', number: 600 },
{ hex: '#be185d', number: 700 },
{ hex: '#9d174d', number: 800 },
{ hex: '#831843', number: 900 },
{ hex: '#500724', number: 950 }
]
},
{
name: 'Rose',
palettes: [
{ hex: '#fff1f2', number: 50 },
{ hex: '#ffe4e6', number: 100 },
{ hex: '#fecdd3', number: 200 },
{ hex: '#fda4af', number: 300 },
{ hex: '#fb7185', number: 400 },
{ hex: '#f43f5e', number: 500 },
{ hex: '#e11d48', number: 600 },
{ hex: '#be123c', number: 700 },
{ hex: '#9f1239', number: 800 },
{ hex: '#881337', number: 900 },
{ hex: '#4c0519', number: 950 }
]
}
];

View File

@@ -0,0 +1,7 @@
import { colorPalettes } from './constant';
export * from './palette';
export * from './shared';
export { colorPalettes };
export * from './types';

View File

@@ -0,0 +1,176 @@
import type { AnyColor, HsvColor } from 'colord';
import { getHex, getHsv, isValidColor, mixColor } from '../shared';
import type { ColorIndex } from '../types';
/** Hue step */
const hueStep = 2;
/** Saturation step, light color part */
const saturationStep = 16;
/** Saturation step, dark color part */
const saturationStep2 = 5;
/** Brightness step, light color part */
const brightnessStep1 = 5;
/** Brightness step, dark color part */
const brightnessStep2 = 15;
/** Light color count, main color up */
const lightColorCount = 5;
/** Dark color count, main color down */
const darkColorCount = 4;
/**
* Get AntD palette color by index
*
* @param color - Color
* @param index - The color index of color palette (the main color index is 6)
* @returns Hex color
*/
export function getAntDPaletteColorByIndex(color: AnyColor, index: ColorIndex): string {
if (!isValidColor(color)) {
throw new Error('invalid input color value');
}
if (index === 6) {
return getHex(color);
}
const isLight = index < 6;
const hsv = getHsv(color);
const i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1;
const newHsv: HsvColor = {
h: getHue(hsv, i, isLight),
s: getSaturation(hsv, i, isLight),
v: getValue(hsv, i, isLight)
};
return getHex(newHsv);
}
/** Map of dark color index and opacity */
const darkColorMap = [
{ index: 7, opacity: 0.15 },
{ index: 6, opacity: 0.25 },
{ index: 5, opacity: 0.3 },
{ index: 5, opacity: 0.45 },
{ index: 5, opacity: 0.65 },
{ index: 5, opacity: 0.85 },
{ index: 5, opacity: 0.9 },
{ index: 4, opacity: 0.93 },
{ index: 3, opacity: 0.95 },
{ index: 2, opacity: 0.97 },
{ index: 1, opacity: 0.98 }
];
/**
* Get AntD color palette
*
* @param color - Color
* @param darkTheme - Dark theme
* @param darkThemeMixColor - Dark theme mix color (default: #141414)
*/
export function getAntDColorPalette(color: AnyColor, darkTheme = false, darkThemeMixColor = '#141414'): string[] {
const indexes: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const patterns = indexes.map(index => getAntDPaletteColorByIndex(color, index));
if (darkTheme) {
const darkPatterns = darkColorMap.map(({ index, opacity }) => {
const darkColor = mixColor(darkThemeMixColor, patterns[index], opacity);
return darkColor;
});
return darkPatterns.map(item => getHex(item));
}
return patterns;
}
/**
* Get hue
*
* @param hsv - Hsv format color
* @param i - The relative distance from 6
* @param isLight - Is light color
*/
function getHue(hsv: HsvColor, i: number, isLight: boolean) {
let hue: number;
const hsvH = Math.round(hsv.h);
if (hsvH >= 60 && hsvH <= 240) {
hue = isLight ? hsvH - hueStep * i : hsvH + hueStep * i;
} else {
hue = isLight ? hsvH + hueStep * i : hsvH - hueStep * i;
}
if (hue < 0) {
hue += 360;
}
if (hue >= 360) {
hue -= 360;
}
return hue;
}
/**
* Get saturation
*
* @param hsv - Hsv format color
* @param i - The relative distance from 6
* @param isLight - Is light color
*/
function getSaturation(hsv: HsvColor, i: number, isLight: boolean) {
if (hsv.h === 0 && hsv.s === 0) {
return hsv.s;
}
let saturation: number;
if (isLight) {
saturation = hsv.s - saturationStep * i;
} else if (i === darkColorCount) {
saturation = hsv.s + saturationStep;
} else {
saturation = hsv.s + saturationStep2 * i;
}
if (saturation > 100) {
saturation = 100;
}
if (isLight && i === lightColorCount && saturation > 10) {
saturation = 10;
}
if (saturation < 6) {
saturation = 6;
}
return saturation;
}
/**
* Get value of hsv
*
* @param hsv - Hsv format color
* @param i - The relative distance from 6
* @param isLight - Is light color
*/
function getValue(hsv: HsvColor, i: number, isLight: boolean) {
let value: number;
if (isLight) {
value = hsv.v + brightnessStep1 * i;
} else {
value = hsv.v - brightnessStep2 * i;
}
if (value > 100) {
value = 100;
}
return value;
}

View File

@@ -0,0 +1,45 @@
import type { AnyColor } from 'colord';
import { getHex } from '../shared';
import type { ColorPaletteNumber } from '../types';
import { getRecommendedColorPalette } from './recommend';
import { getAntDColorPalette } from './antd';
/**
* get color palette by provided color
*
* @param color
* @param recommended whether to get recommended color palette (the provided color may not be the main color)
*/
export function getColorPalette(color: AnyColor, recommended = false) {
const colorMap = new Map<ColorPaletteNumber, string>();
if (recommended) {
const colorPalette = getRecommendedColorPalette(getHex(color));
colorPalette.palettes.forEach(palette => {
colorMap.set(palette.number, palette.hex);
});
} else {
const colors = getAntDColorPalette(color);
const colorNumbers: ColorPaletteNumber[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
colorNumbers.forEach((number, index) => {
colorMap.set(number, colors[index]);
});
}
return colorMap;
}
/**
* get color palette color by number
*
* @param color the provided color
* @param number the color palette number
* @param recommended whether to get recommended color palette (the provided color may not be the main color)
*/
export function getPaletteColorByNumber(color: AnyColor, number: ColorPaletteNumber, recommended = false) {
const colorMap = getColorPalette(color, recommended);
return colorMap.get(number as ColorPaletteNumber)!;
}

View File

@@ -0,0 +1,152 @@
import { getColorName, getDeltaE, getHsl, isValidColor, transformHslToHex } from '../shared';
import { colorPalettes } from '../constant';
import type {
ColorPalette,
ColorPaletteFamily,
ColorPaletteFamilyWithNearestPalette,
ColorPaletteMatch,
ColorPaletteNumber
} from '../types';
/**
* get recommended color palette by provided color
*
* @param color the provided color
*/
export function getRecommendedColorPalette(color: string) {
const colorPaletteFamily = getRecommendedColorPaletteFamily(color);
const colorMap = new Map<ColorPaletteNumber, ColorPalette>();
colorPaletteFamily.palettes.forEach(palette => {
colorMap.set(palette.number, palette);
});
const mainColor = colorMap.get(500)!;
const matchColor = colorPaletteFamily.palettes.find(palette => palette.hex === color)!;
const colorPalette: ColorPaletteMatch = {
...colorPaletteFamily,
colorMap,
main: mainColor,
match: matchColor
};
return colorPalette;
}
/**
* get recommended palette color by provided color
*
* @param color the provided color
* @param number the color palette number
*/
export function getRecommendedPaletteColorByNumber(color: string, number: ColorPaletteNumber) {
const colorPalette = getRecommendedColorPalette(color);
const { hex } = colorPalette.colorMap.get(number)!;
return hex;
}
/**
* get color palette family by provided color and color name
*
* @param color the provided color
*/
export function getRecommendedColorPaletteFamily(color: string) {
if (!isValidColor(color)) {
throw new Error('Invalid color, please check color value!');
}
let colorName = getColorName(color);
colorName = colorName.toLowerCase().replace(/\s/g, '-');
const { h: h1, s: s1 } = getHsl(color);
const { nearestLightnessPalette, palettes } = getNearestColorPaletteFamily(color, colorPalettes);
const { number, hex } = nearestLightnessPalette;
const { h: h2, s: s2 } = getHsl(hex);
const deltaH = h1 - h2;
const sRatio = s1 / s2;
const colorPaletteFamily: ColorPaletteFamily = {
name: colorName,
palettes: palettes.map(palette => {
let hexValue = color;
const isSame = number === palette.number;
if (!isSame) {
const { h: h3, s: s3, l } = getHsl(palette.hex);
const newH = deltaH < 0 ? h3 + deltaH : h3 - deltaH;
const newS = s3 * sRatio;
hexValue = transformHslToHex({
h: newH,
s: newS,
l
});
}
return {
hex: hexValue,
number: palette.number
};
})
};
return colorPaletteFamily;
}
/**
* get nearest color palette family
*
* @param color color
* @param families color palette families
*/
function getNearestColorPaletteFamily(color: string, families: ColorPaletteFamily[]) {
const familyWithConfig = families.map(family => {
const palettes = family.palettes.map(palette => {
return {
...palette,
delta: getDeltaE(color, palette.hex)
};
});
const nearestPalette = palettes.reduce((prev, curr) => (prev.delta < curr.delta ? prev : curr));
return {
...family,
palettes,
nearestPalette
};
});
const nearestPaletteFamily = familyWithConfig.reduce((prev, curr) =>
prev.nearestPalette.delta < curr.nearestPalette.delta ? prev : curr
);
const { l } = getHsl(color);
const paletteFamily: ColorPaletteFamilyWithNearestPalette = {
...nearestPaletteFamily,
nearestLightnessPalette: nearestPaletteFamily.palettes.reduce((prev, curr) => {
const { l: prevLightness } = getHsl(prev.hex);
const { l: currLightness } = getHsl(curr.hex);
const deltaPrev = Math.abs(prevLightness - l);
const deltaCurr = Math.abs(currLightness - l);
return deltaPrev < deltaCurr ? prev : curr;
})
};
return paletteFamily;
}

View File

@@ -0,0 +1,93 @@
import { colord, extend } from 'colord';
import namesPlugin from 'colord/plugins/names';
import mixPlugin from 'colord/plugins/mix';
import labPlugin from 'colord/plugins/lab';
import type { AnyColor, HslColor, RgbColor } from 'colord';
extend([namesPlugin, mixPlugin, labPlugin]);
export function isValidColor(color: AnyColor) {
return colord(color).isValid();
}
export function getHex(color: AnyColor) {
return colord(color).toHex();
}
export function getRgb(color: AnyColor) {
return colord(color).toRgb();
}
export function getHsl(color: AnyColor) {
return colord(color).toHsl();
}
export function getHsv(color: AnyColor) {
return colord(color).toHsv();
}
export function getDeltaE(color1: AnyColor, color2: AnyColor) {
return colord(color1).delta(color2);
}
export function transformHslToHex(color: HslColor) {
return colord(color).toHex();
}
/**
* Add color alpha
*
* @param color - Color
* @param alpha - Alpha (0 - 1)
*/
export function addColorAlpha(color: AnyColor, alpha: number) {
return colord(color).alpha(alpha).toHex();
}
/**
* Mix color
*
* @param firstColor - First color
* @param secondColor - Second color
* @param ratio - The ratio of the second color (0 - 1)
*/
export function mixColor(firstColor: AnyColor, secondColor: AnyColor, ratio: number) {
return colord(firstColor).mix(secondColor, ratio).toHex();
}
/**
* Transform color with opacity to similar color without opacity
*
* @param color - Color
* @param alpha - Alpha (0 - 1)
* @param bgColor Background color (usually white or black)
*/
export function transformColorWithOpacity(color: AnyColor, alpha: number, bgColor = '#ffffff') {
const originColor = addColorAlpha(color, alpha);
const { r: oR, g: oG, b: oB } = colord(originColor).toRgb();
const { r: bgR, g: bgG, b: bgB } = colord(bgColor).toRgb();
function calRgb(or: number, bg: number, al: number) {
return bg + (or - bg) * al;
}
const resultRgb: RgbColor = {
r: calRgb(oR, bgR, alpha),
g: calRgb(oG, bgG, alpha),
b: calRgb(oB, bgB, alpha)
};
return colord(resultRgb).toHex();
}
/**
* Is white color
*
* @param color - Color
*/
export function isWhiteColor(color: AnyColor) {
return colord(color).isEqual('#ffffff');
}
export { colord };

View File

@@ -0,0 +1,2 @@
export * from './colord';
export * from './name';

View File

@@ -1,6 +1,11 @@
import { getHex, getRgb, getHsl } from './color';
import colorNames from './json/color-name.json';
import { colorNames } from '../constant';
import { getHex, getHsl, getRgb } from './colord';
/**
* Get color name
*
* @param color
*/
export function getColorName(color: string) {
const hex = getHex(color);
const rgb = getRgb(color);
@@ -17,15 +22,13 @@ export function getColorName(color: string) {
colorNames.some((item, index) => {
const [hexValue, colorName] = item;
const hexcode = `#${hexValue}`;
const match = hex === hexcode;
const match = hex === hexValue;
if (match) {
name = colorName;
} else {
const { r, g, b } = getRgb(hexcode);
const { h, s, l } = getHsl(hexcode);
const { r, g, b } = getRgb(hexValue);
const { h, s, l } = getHsl(hexValue);
ndf1 = (rgb.r - r) ** 2 + (rgb.g - g) ** 2 + (rgb.b - b) ** 2;
ndf2 = (hsl.h - h) ** 2 + (hsl.s - s) ** 2 + (hsl.l - l) ** 2;
@@ -40,7 +43,7 @@ export function getColorName(color: string) {
return match;
});
name = cl < 0 ? 'Invalid Color' : colorNames[cl][1];
name = colorNames[cl][1];
return name;
}

View File

@@ -0,0 +1,58 @@
/**
* the color palette number
*
* the main color number is 500
*/
export type ColorPaletteNumber = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
/** the color palette */
export type ColorPalette = {
/** the color hex value */
hex: string;
/**
* the color number
*
* - 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950
*/
number: ColorPaletteNumber;
};
/** the color palette family */
export type ColorPaletteFamily = {
/** the color palette family name */
name: string;
/** the color palettes */
palettes: ColorPalette[];
};
/** the color palette with delta */
export type ColorPaletteWithDelta = ColorPalette & {
delta: number;
};
/** the color palette family with nearest palette */
export type ColorPaletteFamilyWithNearestPalette = ColorPaletteFamily & {
nearestPalette: ColorPaletteWithDelta;
nearestLightnessPalette: ColorPaletteWithDelta;
};
/** the color palette match */
export type ColorPaletteMatch = ColorPaletteFamily & {
/** the color map of the palette */
colorMap: Map<ColorPaletteNumber, ColorPalette>;
/**
* the main color of the palette
*
* which number is 500
*/
main: ColorPalette;
/** the match color of the palette */
match: ColorPalette;
};
/**
* The color index of color palette
*
* From left to right, the color is from light to dark, 6 is main color
*/
export type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,38 +0,0 @@
import path from 'node:path';
import { defineConfig } from 'vitepress';
export default defineConfig({
title: 'Soybean Admin',
description: '一个优雅、清新、漂亮的中后台模版',
head: [
['meta', { name: 'author', content: 'Soybean' }],
[
'meta',
{
name: 'keywords',
content: 'soybean, soybean-admin, vite, vue, vue3, soybean-admin docs'
}
],
['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }],
[
'meta',
{
name: 'viewport',
content: 'width=device-width,initial-scale=1,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no'
}
],
['link', { rel: 'icon', href: '/favicon.ico' }]
],
srcDir: path.join(process.cwd(), 'packages/docs/src'),
themeConfig: {
logo: '/logo.svg',
socialLinks: [{ icon: 'github', link: 'https://github.com/honghuangdc/soybean-admin' }],
algolia: {
appId: '98WN1RY04S',
apiKey: '13e9f5767b774422a5880723d9c23265',
indexName: 'soybean'
},
nav: [],
sidebar: {}
}
});

View File

@@ -1,32 +0,0 @@
export const qqSvg = `
<svg height="2500" viewBox="-1.94 0 124.879 145.085" width="2101" xmlns="http://www.w3.org/2000/svg">
<path
d="m60.503 142.237c-12.533 0-24.038-4.195-31.445-10.46-3.762 1.124-8.574 2.932-11.61 5.175-2.6 1.918-2.275 3.874-1.807 4.663 2.056 3.47 35.273 2.216 44.862 1.136zm0 0c12.535 0 24.039-4.195 31.447-10.46 3.76 1.124 8.573 2.932 11.61 5.175 2.598 1.918 2.274 3.874 1.805 4.663-2.056 3.47-35.272 2.216-44.862 1.136zm0 0"
fill="#faab07"
/>
<path
d="m60.576 67.119c20.698-.14 37.286-4.147 42.907-5.683 1.34-.367 2.056-1.024 2.056-1.024.005-.189.085-3.37.085-5.01 0-27.634-13.044-55.401-45.124-55.402-32.08.001-45.125 27.769-45.125 55.401 0 1.642.08 4.822.086 5.01 0 0 .583.615 1.65.913 5.19 1.444 22.09 5.65 43.312 5.795zm56.245 23.02c-1.283-4.129-3.034-8.944-4.808-13.568 0 0-1.02-.126-1.537.023-15.913 4.623-35.202 7.57-49.9 7.392h-.153c-14.616.175-33.774-2.737-49.634-7.315-.606-.175-1.802-.1-1.802-.1-1.774 4.624-3.525 9.44-4.808 13.568-6.119 19.69-4.136 27.838-2.627 28.02 3.239.392 12.606-14.821 12.606-14.821 0 15.459 13.957 39.195 45.918 39.413h.848c31.96-.218 45.917-23.954 45.917-39.413 0 0 9.368 15.213 12.607 14.822 1.508-.183 3.491-8.332-2.627-28.021"
/>
<path
d="m49.085 40.824c-4.352.197-8.07-4.76-8.304-11.063-.236-6.305 3.098-11.576 7.45-11.773 4.347-.195 8.064 4.76 8.3 11.065.238 6.306-3.097 11.577-7.446 11.771m31.133-11.063c-.233 6.302-3.951 11.26-8.303 11.063-4.35-.195-7.684-5.465-7.446-11.77.236-6.305 3.952-11.26 8.3-11.066 4.352.197 7.686 5.468 7.449 11.773"
fill="#fff"
/>
<path
d="m87.952 49.725c-1.162-2.575-12.875-5.445-27.374-5.445h-.156c-14.5 0-26.212 2.87-27.375 5.446a.863.863 0 0 0 -.085.367c0 .186.063.352.16.496.98 1.427 13.985 8.487 27.3 8.487h.156c13.314 0 26.319-7.058 27.299-8.487a.873.873 0 0 0 .16-.498.856.856 0 0 0 -.085-.365"
fill="#faab07"
/>
<path
d="m54.434 29.854c.199 2.49-1.167 4.702-3.046 4.943-1.883.242-3.568-1.58-3.768-4.07-.197-2.492 1.167-4.704 3.043-4.944 1.886-.244 3.574 1.58 3.771 4.07m11.956.833c.385-.689 3.004-4.312 8.427-2.993 1.425.347 2.084.857 2.223 1.057.205.296.262.718.053 1.286-.412 1.126-1.263 1.095-1.734.875-.305-.142-4.082-2.66-7.562 1.097-.24.257-.668.346-1.073.04-.407-.308-.574-.93-.334-1.362"
/>
<path
d="m60.576 83.08h-.153c-9.996.12-22.116-1.204-33.854-3.518-1.004 5.818-1.61 13.132-1.09 21.853 1.316 22.043 14.407 35.9 34.614 36.1h.82c20.208-.2 33.298-14.057 34.616-36.1.52-8.723-.087-16.035-1.092-21.854-11.739 2.315-23.862 3.64-33.86 3.518"
fill="#fff"
/>
<g fill="#eb1923">
<path d="m32.102 81.235v21.693s9.937 2.004 19.893.616v-20.009c-6.307-.357-13.109-1.152-19.893-2.3" />
<path
d="m105.539 60.412s-19.33 6.102-44.963 6.275h-.153c-25.591-.172-44.896-6.255-44.962-6.275l-6.474 16.158c16.193 4.882 36.261 8.028 51.436 7.845h.153c15.175.183 35.242-2.963 51.437-7.845zm0 0"
/>
</g>
</svg>
`;

View File

@@ -1,4 +0,0 @@
import Theme from 'vitepress/theme';
import './style.css';
export default Theme;

View File

@@ -1,90 +0,0 @@
/**
* Customize default theme styling by overriding CSS variables:
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
*/
/**
* Colors
* -------------------------------------------------------------------------- */
:root {
--vp-c-brand: #646cff;
--vp-c-brand-light: #747bff;
--vp-c-brand-lighter: #9499ff;
--vp-c-brand-lightest: #bcc0ff;
--vp-c-brand-dark: #535bf2;
--vp-c-brand-darker: #454ce1;
--vp-c-brand-dimm: rgba(100, 108, 255, 0.08);
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root {
--vp-button-brand-border: var(--vp-c-brand-light);
--vp-button-brand-text: var(--vp-c-white);
--vp-button-brand-bg: var(--vp-c-brand);
--vp-button-brand-hover-border: var(--vp-c-brand-light);
--vp-button-brand-hover-text: var(--vp-c-white);
--vp-button-brand-hover-bg: var(--vp-c-brand-light);
--vp-button-brand-active-border: var(--vp-c-brand-light);
--vp-button-brand-active-text: var(--vp-c-white);
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
var(--vp-c-brand-lightest) 30%,
var(--vp-c-brand-darker)
);
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
var(--vp-c-brand-lightest) 30%,
var(--vp-c-brand) 50%
);
--vp-home-hero-image-filter: blur(40px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(72px);
}
}
/**
* Component: Custom Block
* -------------------------------------------------------------------------- */
:root {
--vp-custom-block-tip-border: var(--vp-c-brand);
--vp-custom-block-tip-text: var(--vp-c-brand-darker);
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
}
.dark {
--vp-custom-block-tip-border: var(--vp-c-brand);
--vp-custom-block-tip-text: var(--vp-c-brand-lightest);
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm);
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand) !important;
}

View File

@@ -1,12 +0,0 @@
{
"name": "@sa/docs",
"version": "1.0.0",
"scripts": {
"dev": "vitepress dev",
"build": "vitepress build",
"serve": "vitepress serve"
},
"devDependencies": {
"vitepress": "1.0.0-rc.24"
}
}

View File

@@ -1,44 +0,0 @@
---
layout: home
title: Soybean Admin
titleTemplate: 一个清新优雅的中后台模版
hero:
name: Soybean Admin
text: 清新优雅的中后台模版
tagline: 基于 Vue3 + Vite3 + TS + NaiveUI + UnoCSS
image:
src: /logo.svg
alt: Soybean Admin
actions:
- theme: brand
text: 开始
link: /guide/
- theme: alt
text: 介绍
link: /guide/introduction
- theme: alt
text: 在 GitHub 上查看
link: https://github.com/honghuangdc/soybean-admin
features:
- icon: 🆕
title: 最新流行技术栈
details: 基于Vue3、Vite3、TS、NaiveUI和UnoCSS等最新技术栈开发
- icon: 🦋
title: 极高水准的代码规范
details: 代码规范完善,代码结构清晰
- icon: 🛠️
title: 丰富的插件
details: 常见的Web端插件示例实现
- icon: 🔩
title: 主题配置
details: 丰富的主题配置及暗黑主题适配
- icon: 🔗
title: 基于文件的路由系统
details: 自动生成路由声明、路由导入和路由模块
- icon: 🔑
title: 权限管理
details: 完善的前后端权限管理方案
---

View File

@@ -1,6 +0,0 @@
/**
* @type {import('eslint').ESLint.ConfigData}
*/
module.exports = {
extends: [require.resolve('./ts.js'), require.resolve('./prettier.js')]
};

View File

@@ -1,44 +0,0 @@
/**
* @type {import('eslint').ESLint.ConfigData}
*/
module.exports = {
root: true,
env: {
browser: true,
node: true,
commonjs: true,
es2024: true
},
parserOptions: {
ecmaVersion: 2024,
ecmaFeatures: {
jsx: true
},
sourceType: 'module'
},
ignorePatterns: [
'node_modules',
'*.min.*',
'CHANGELOG.md',
'dist',
'LICENSE*',
'output',
'coverage',
'public',
'temp',
'package-lock.json',
'pnpm-lock.yaml',
'yarn.lock',
'__snapshots__',
'!.github',
'!.vitepress',
'!.vscode'
],
plugins: ['n', 'promise'],
extends: [require.resolve('../rules/all.js'), 'plugin:import/recommended'],
rules: {
// import
'import/no-mutable-exports': 'error',
'import/no-named-as-default': 'off'
}
};

View File

@@ -1,11 +0,0 @@
const prettierRules = require('../rules/prettier');
/**
* @type {import('eslint').ESLint.ConfigData}
*/
module.exports = {
extends: ['plugin:prettier/recommended'],
rules: {
'prettier/prettier': ['error', prettierRules]
}
};

View File

@@ -1,61 +0,0 @@
/**
* @type {import('eslint').ESLint.ConfigData}
*/
module.exports = {
plugins: ['@typescript-eslint'],
extends: [require.resolve('./js.js'), 'plugin:import/typescript', 'plugin:@typescript-eslint/recommended'],
settings: {
'import/resolver': {
typescript: {
project: ['tsconfig.json', 'packages/*/tsconfig.json', 'examples/*/tsconfig.json', 'docs/*/tsconfig.json']
}
}
},
overrides: [
{
files: ['*.ts', '*.tsx', '*.mts', '*.cts'],
parser: '@typescript-eslint/parser'
},
{
files: ['*.js', '*.mjs', '*.cjs', '*.cts'],
rules: {
'@typescript-eslint/no-var-requires': 'off'
}
}
],
rules: {
// TS
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', disallowTypeAnnotations: false }],
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true
}
],
// Override JS
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
vars: 'all',
args: 'all',
ignoreRestSiblings: false,
varsIgnorePattern: '^_',
argsIgnorePattern: '^_'
}
],
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': ['error', { functions: false, classes: false, variables: true }],
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
// off
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off'
}
};

View File

@@ -1,30 +0,0 @@
/**
* @type {import('eslint').ESLint.ConfigData}
*/
module.exports = {
extends: ['plugin:vue/vue3-recommended', require.resolve('./base.js')],
overrides: [
{
files: ['*.vue'],
parser: 'vue-eslint-parser',
parserOptions: {
parser: {
js: 'espree',
jsx: 'espree',
ts: '@typescript-eslint/parser',
tsx: '@typescript-eslint/parser'
},
extraFileExtensions: ['.vue'],
ecmaFeatures: {
jsx: true
}
},
rules: {
'no-undef': 'off' // TS will check un declared variables, if the script code is is in a .vue file, this rule should not disabled
}
}
],
rules: {
'vue/multi-word-component-names': 'off'
}
};

View File

@@ -1,6 +0,0 @@
const baseConfig = require('./configs/base');
/**
* @type {import('eslint').ESLint.ConfigData}
*/
module.exports = baseConfig;

View File

@@ -1,23 +0,0 @@
{
"name": "eslint-config-sa",
"version": "1.0.0",
"description": "SoybeanAdmin's eslint config resets",
"exports": {
".": "./index.js",
"./vue": "./configs/vue.js"
},
"devDependencies": {
"@types/eslint": "8.44.6",
"@typescript-eslint/eslint-plugin": "6.9.0",
"@typescript-eslint/parser": "6.9.0",
"eslint": "8.52.0",
"eslint-config-prettier": "9.0.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.0",
"eslint-plugin-n": "16.2.0",
"eslint-plugin-prettier": "5.0.1",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.18.0",
"prettier": "3.0.3"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
/**
* @type {import('prettier').Options}
*/
module.exports = {
printWidth: 120,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
quoteProps: 'as-needed',
jsxSingleQuote: false,
trailingComma: 'none',
bracketSpacing: true,
bracketSameLine: false,
arrowParens: 'avoid',
rangeStart: 0,
rangeEnd: Number.POSITIVE_INFINITY,
requirePragma: false,
insertPragma: false,
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'ignore',
vueIndentScriptAndStyle: false,
endOfLine: 'lf',
embeddedLanguageFormatting: 'auto',
singleAttributePerLine: false
};

View File

@@ -1,14 +1,16 @@
{
"name": "@sa/hooks",
"version": "1.0.0",
"version": "1.3.7",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": [
"./src/*"
]
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/axios": "workspace:*",
"@sa/utils": "workspace:*"
}
}

View File

@@ -1,5 +1,11 @@
import useBoolean from './use-boolean';
import useLoading from './use-loading';
import useCountDown from './use-count-down';
import useContext from './use-context';
import useSvgIconRender from './use-svg-icon-render';
import useHookTable from './use-table';
export { useBoolean, useLoading, useContext };
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
export * from './use-signal';
export * from './use-table';

View File

@@ -1,8 +1,9 @@
import { ref } from 'vue';
/**
* boolean
* @param initValue init value
* Boolean
*
* @param initValue Init value
*/
export default function useBoolean(initValue = false) {
const bool = ref(initValue);

View File

@@ -1,93 +1,81 @@
import { inject, provide } from 'vue';
import type { InjectionKey } from 'vue';
type ContextFn<T> = () => T;
/**
* use context
* @param contextName context name
* @param fn context function
* Use context
*
* @example
* ```ts
* // there are three vue files: A.vue, B.vue, C.vue, and A.vue is the parent component of B.vue and C.vue
* ```ts
* // there are three vue files: A.vue, B.vue, C.vue, and A.vue is the parent component of B.vue and C.vue
*
* // context.ts
* import { ref } from 'vue';
* import { useContext } from '@sa/hooks';
* // context.ts
* import { ref } from 'vue';
* import { useContext } from '@sa/hooks';
*
* export const { setupStore, useStore } = useContext('demo', () => {
* const count = ref(0);
* export const { setupStore, useStore } = useContext('demo', () => {
* const count = ref(0);
*
* function increment() {
* count.value++;
* }
* function increment() {
* count.value++;
* }
*
* function decrement() {
* count.value--;
* }
* function decrement() {
* count.value--;
* }
*
* return {
* count,
* increment,
* decrement
* };
* })
* ```
* return {
* count,
* increment,
* decrement
* };
* })
* ``` // A.vue
* ```vue
* <template>
* <div>A</div>
* </template>
* <script setup lang="ts">
* import { setupStore } from './context';
*
* // A.vue
* ```vue
* <template>
* <div>A</div>
* </template>
* <script setup lang="ts">
* import { setupStore } from './context';
* setupStore();
* // const { increment } = setupStore(); // also can control the store in the parent component
* </script>
* ``` // B.vue
* ```vue
* <template>
* <div>B</div>
* </template>
* <script setup lang="ts">
* import { useStore } from './context';
*
* setupStore();
* // const { increment } = setupStore(); // also can control the store in the parent component
* </script>
* ```
* // B.vue
* ```vue
* <template>
* <div>B</div>
* </template>
* <script setup lang="ts">
* import { useStore } from './context';
* const { count, increment } = useStore();
* </script>
* ```;
*
* const { count, increment } = useStore();
* </script>
* ```
* // C.vue is same as B.vue
*
* // C.vue is same as B.vue
* @param contextName Context name
* @param fn Context function
*/
export default function useContext<T>(contextName: string, fn: ContextFn<T>) {
const { useProvide, useInject } = createContext<T>(contextName);
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
type Context = ReturnType<T>;
const context = fn();
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
/**
* setup store in the parent component
*/
function setupStore() {
function setupStore(...args: Parameters<T>) {
const context: Context = fn(...args);
return useProvide(context);
}
/**
* use store in the child component
*/
function useStore() {
return useInject();
}
return {
/** Setup store in the parent component */
setupStore,
/** Use store in the child component */
useStore
};
}
/**
* create context
*/
/** Create context */
function createContext<T>(contextName: string) {
const injectKey: InjectionKey<T> = Symbol(contextName);

View File

@@ -0,0 +1,49 @@
import { computed, onScopeDispose, ref } from 'vue';
import { useRafFn } from '@vueuse/core';
/**
* count down
*
* @param seconds - count down seconds
*/
export default function useCountDown(seconds: number) {
const FPS_PER_SECOND = 60;
const fps = ref(0);
const count = computed(() => Math.ceil(fps.value / FPS_PER_SECOND));
const isCounting = computed(() => fps.value > 0);
const { pause, resume } = useRafFn(
() => {
if (fps.value > 0) {
fps.value -= 1;
} else {
pause();
}
},
{ immediate: false }
);
function start(updateSeconds: number = seconds) {
fps.value = FPS_PER_SECOND * updateSeconds;
resume();
}
function stop() {
fps.value = 0;
pause();
}
onScopeDispose(() => {
pause();
});
return {
count,
isCounting,
start,
stop
};
}

View File

@@ -1,8 +1,9 @@
import useBoolean from './use-boolean';
/**
* loading
* @param initValue init value
* Loading
*
* @param initValue Init value
*/
export default function useLoading(initValue = false) {
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue);

View File

@@ -0,0 +1,79 @@
import { ref } from 'vue';
import type { Ref } from 'vue';
import { createFlatRequest } from '@sa/axios';
import type {
AxiosError,
CreateAxiosDefaults,
CustomAxiosRequestConfig,
MappedType,
RequestOption,
ResponseType
} from '@sa/axios';
import useLoading from './use-loading';
export type HookRequestInstanceResponseSuccessData<T = any> = {
data: Ref<T>;
error: Ref<null>;
};
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
data: Ref<null>;
error: Ref<AxiosError<ResponseData>>;
};
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
loading: Ref<boolean>;
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
export interface HookRequestInstance<ResponseData = any> {
<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
cancelRequest: (requestId: string) => void;
cancelAllRequest: () => void;
}
/**
* create a hook request instance
*
* @param axiosConfig
* @param options
*/
export default function createHookRequest<ResponseData = any>(
axiosConfig?: CreateAxiosDefaults,
options?: Partial<RequestOption<ResponseData>>
) {
const request = createFlatRequest<ResponseData>(axiosConfig, options);
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
config: CustomAxiosRequestConfig
) {
const { loading, startLoading, endLoading } = useLoading();
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
startLoading();
request(config).then(res => {
if (res.data) {
data.value = res.data;
} else {
error.value = res.error;
}
endLoading();
});
return {
loading,
data,
error
};
} as HookRequestInstance<ResponseData>;
hookRequest.cancelRequest = request.cancelRequest;
hookRequest.cancelAllRequest = request.cancelAllRequest;
return hookRequest;
}

View File

@@ -0,0 +1,144 @@
import { computed, ref, shallowRef, triggerRef } from 'vue';
import type {
ComputedGetter,
DebuggerOptions,
Ref,
ShallowRef,
WritableComputedOptions,
WritableComputedRef
} from 'vue';
type Updater<T> = (value: T) => T;
type Mutator<T> = (value: T) => void;
/**
* Signal is a reactive value that can be set, updated or mutated
*
* @example
* ```ts
* const count = useSignal(0);
*
* // `watchEffect`
* watchEffect(() => {
* console.log(count());
* });
*
* // watch
* watch(count, value => {
* console.log(value);
* });
*
* // useComputed
* const double = useComputed(() => count() * 2);
* const writeableDouble = useComputed({
* get: () => count() * 2,
* set: value => count.set(value / 2)
* });
* ```
*/
export interface Signal<T> {
(): Readonly<T>;
/**
* Set the value of the signal
*
* It recommend use `set` for primitive values
*
* @param value
*/
set(value: T): void;
/**
* Update the value of the signal using an updater function
*
* It recommend use `update` for non-primitive values, only the first level of the object will be reactive.
*
* @param updater
*/
update(updater: Updater<T>): void;
/**
* Mutate the value of the signal using a mutator function
*
* this action will call `triggerRef`, so the value will be tracked on `watchEffect`.
*
* It recommend use `mutate` for non-primitive values, all levels of the object will be reactive.
*
* @param mutator
*/
mutate(mutator: Mutator<T>): void;
/**
* Get the reference of the signal
*
* Sometimes it can be useful to make `v-model` work with the signal
*
* ```vue
* <template>
* <input v-model="model.count" />
* </template>;
*
* <script setup lang="ts">
* const state = useSignal({ count: 0 }, { useRef: true });
*
* const model = state.getRef();
* </script>
* ```
*/
getRef(): Readonly<ShallowRef<Readonly<T>>>;
}
export interface ReadonlySignal<T> {
(): Readonly<T>;
}
export interface SignalOptions {
/**
* Whether to use `ref` to store the value
*
* @default false use `sharedRef` to store the value
*/
useRef?: boolean;
}
export function useSignal<T>(initialValue: T, options?: SignalOptions): Signal<T> {
const { useRef } = options || {};
const state = useRef ? (ref(initialValue) as Ref<T>) : shallowRef(initialValue);
return createSignal(state);
}
export function useComputed<T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ReadonlySignal<T>;
export function useComputed<T>(options: WritableComputedOptions<T>, debugOptions?: DebuggerOptions): Signal<T>;
export function useComputed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
) {
const isGetter = typeof getterOrOptions === 'function';
const computedValue = computed(getterOrOptions as any, debugOptions);
if (isGetter) {
return () => computedValue.value as ReadonlySignal<T>;
}
return createSignal(computedValue);
}
function createSignal<T>(state: ShallowRef<T> | WritableComputedRef<T>): Signal<T> {
const signal = () => state.value;
signal.set = (value: T) => {
state.value = value;
};
signal.update = (updater: Updater<T>) => {
state.value = updater(state.value);
};
signal.mutate = (mutator: Mutator<T>) => {
mutator(state.value);
triggerRef(state);
};
signal.getRef = () => state as Readonly<ShallowRef<Readonly<T>>>;
return signal;
}

View File

@@ -0,0 +1,50 @@
import { h } from 'vue';
import type { Component } from 'vue';
/**
* Svg icon render hook
*
* @param SvgIcon Svg icon component
*/
export default function useSvgIconRender(SvgIcon: Component) {
interface IconConfig {
/** Iconify icon name */
icon?: string;
/** Local icon name */
localIcon?: string;
/** Icon color */
color?: string;
/** Icon size */
fontSize?: number;
}
type IconStyle = Partial<Pick<CSSStyleDeclaration, 'color' | 'fontSize'>>;
/**
* Svg icon VNode
*
* @param config
*/
const SvgIconVNode = (config: IconConfig) => {
const { color, fontSize, icon, localIcon } = config;
const style: IconStyle = {};
if (color) {
style.color = color;
}
if (fontSize) {
style.fontSize = `${fontSize}px`;
}
if (!icon && !localIcon) {
return undefined;
}
return () => h(SvgIcon, { icon, localIcon, style });
};
return {
SvgIconVNode
};
}

View File

@@ -0,0 +1,152 @@
import { computed, reactive, ref } from 'vue';
import type { Ref } from 'vue';
import { jsonClone } from '@sa/utils';
import useBoolean from './use-boolean';
import useLoading from './use-loading';
export type MaybePromise<T> = T | Promise<T>;
export type ApiFn = (args: any) => Promise<unknown>;
export type TableColumnCheck = {
prop: string;
label: string;
checked: boolean;
};
export type TableDataWithIndex<T> = T & { index: number };
export type TransformedData<T> = {
data: TableDataWithIndex<T>[];
pageNum: number;
pageSize: number;
total: number;
};
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
export type TableConfig<A extends ApiFn, T, C> = {
/** api function to get table data */
apiFn: A;
/** api params */
apiParams?: Parameters<A>[0];
/** transform api response to table data */
transformer: Transformer<T, Awaited<ReturnType<A>>>;
/** columns factory */
columns: () => C[];
/**
* get column checks
*
* @param columns
*/
getColumnChecks: (columns: C[]) => TableColumnCheck[];
/**
* get columns
*
* @param columns
*/
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
/**
* callback when response fetched
*
* @param transformed transformed data
*/
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
/**
* whether to get data immediately
*
* @default true
*/
immediate?: boolean;
};
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
const { loading, startLoading, endLoading } = useLoading();
const { bool: empty, setBool: setEmpty } = useBoolean();
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
const searchParams: NonNullable<Parameters<A>[0]> = reactive(jsonClone({ ...apiParams }));
const allColumns = ref(config.columns()) as Ref<C[]>;
const data: Ref<TableDataWithIndex<T>[]> = ref([]);
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
function reloadColumns() {
allColumns.value = config.columns();
const checkMap = new Map(columnChecks.value.map(col => [col.prop, col.checked]));
const defaultChecks = getColumnChecks(allColumns.value);
columnChecks.value = defaultChecks.map(col => ({
...col,
checked: checkMap.get(col.prop) ?? col.checked
}));
}
async function getData() {
startLoading();
const formattedParams = formatSearchParams(searchParams);
const response = await apiFn(formattedParams);
const transformed = transformer(response as Awaited<ReturnType<A>>);
data.value = transformed.data;
setEmpty(transformed.data.length === 0);
await config.onFetched?.(transformed);
endLoading();
}
function formatSearchParams(params: Record<string, unknown>) {
const formattedParams: Record<string, unknown> = {};
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
formattedParams[key] = value;
}
});
return formattedParams;
}
/**
* update search params
*
* @param params
*/
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
Object.assign(searchParams, params);
}
/** reset search params */
function resetSearchParams() {
Object.assign(searchParams, jsonClone(apiParams));
}
if (immediate) {
getData();
}
return {
loading,
empty,
data,
columns,
columnChecks,
reloadColumns,
getData,
searchParams,
updateSearchParams,
resetSearchParams
};
}

View File

@@ -1,19 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"target": "ESNext",
"lib": ["DOM", "ESNext"],
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"jsx": "preserve",
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"types": ["node"]
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]

View File

@@ -1,6 +0,0 @@
{
"extends": "sa/vue",
"rules": {
"vue/multi-word-component-names": "off"
}
}

View File

@@ -1,20 +1,19 @@
{
"name": "@sa/materials",
"version": "1.0.0",
"version": "1.3.7",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": [
"./src/*"
]
"*": ["./src/*"]
}
},
"dependencies": {
"@sa/utils": "workspace:*"
"@sa/utils": "workspace:*",
"simplebar-vue": "2.3.5"
},
"devDependencies": {
"typed-css-modules": "0.8.0"
"typed-css-modules": "0.9.1"
}
}

View File

@@ -1,5 +1,6 @@
import AdminLayout from './libs/admin-layout';
import AdminLayout, { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './libs/admin-layout';
import PageTab from './libs/page-tab';
import SimpleScrollbar from './libs/simple-scrollbar';
export { AdminLayout, PageTab };
export { AdminLayout, LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, PageTab, SimpleScrollbar };
export * from './types';

View File

@@ -14,4 +14,5 @@ declare const styles: {
readonly 'sider-padding-top': string;
readonly 'sider-padding-bottom': string;
};
export = styles;
export default styles;

View File

@@ -1,3 +1,5 @@
import AdminLayout from './index.vue';
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID } from './shared';
export default AdminLayout;
export { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX };

View File

@@ -1,130 +1,7 @@
<template>
<div :class="['relative h-full', commonClass]" :style="cssVars">
<div
:id="isWrapperScroll ? scrollElId : undefined"
:class="['flex flex-col h-full', commonClass, scrollWrapperClass, { 'overflow-y-auto': isWrapperScroll }]"
>
<!-- Header -->
<template v-if="showHeader">
<header
v-show="!fullContent"
:class="[
style['layout-header'],
'flex-shrink-0',
commonClass,
headerClass,
headerLeftGapClass,
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
]"
>
<slot name="header"></slot>
</header>
<div
v-show="!fullContent && fixedHeaderAndTab"
:class="[style['layout-header-placement'], 'flex-shrink-0 overflow-hidden']"
></div>
</template>
<!-- Tab -->
<template v-if="showTab">
<div
v-show="!fullContent"
:class="[
style['layout-tab'],
'flex-shrink-0',
commonClass,
tabClass,
{ 'top-0!': !showHeader },
leftGapClass,
{ 'absolute left-0 w-full': fixedHeaderAndTab }
]"
>
<slot name="tab"></slot>
</div>
<div
v-show="!fullContent && fixedHeaderAndTab"
:class="[style['layout-tab-placement'], 'flex-shrink-0 overflow-hidden']"
></div>
</template>
<!-- Sider -->
<template v-if="showSider">
<aside
v-show="!fullContent"
:class="[
'absolute left-0 top-0 h-full',
commonClass,
siderClass,
siderPaddingClass,
siderCollapse ? style['layout-sider_collapsed'] : style['layout-sider']
]"
>
<slot name="sider"></slot>
</aside>
</template>
<!-- Mobile Sider -->
<template v-if="showMobileSider">
<aside
:class="[
'absolute left-0 top-0 w-0 h-full bg-white',
commonClass,
mobileSiderClass,
style['layout-mobile-sider'],
siderCollapse ? 'overflow-hidden' : style['layout-sider']
]"
>
<slot name="sider"></slot>
</aside>
<div
v-show="!siderCollapse"
:class="['absolute left-0 top-0 w-full h-full bg-[rgba(0,0,0,0.2)]', style['layout-mobile-sider-mask']]"
@click="handleClickMask"
></div>
</template>
<!-- Main Content -->
<main
:id="isContentScroll ? scrollElId : undefined"
:class="[
'flex flex-col flex-grow',
commonClass,
contentClass,
leftGapClass,
{ 'overflow-y-auto': isContentScroll }
]"
>
<slot></slot>
</main>
<!-- Footer -->
<template v-if="showFooter">
<footer
v-show="!fullContent"
:class="[
style['layout-footer'],
'flex-shrink-0',
commonClass,
footerClass,
footerLeftGapClass,
{ 'absolute left-0 bottom-0 w-full': fixedFooter }
]"
>
<slot name="footer"></slot>
</footer>
<div
v-show="!fullContent && fixedFooter"
:class="[style['layout-footer-placement'], 'flex-shrink-0 overflow-hidden']"
></div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import type { AdminLayoutProps } from '../../types';
import { LAYOUT_SCROLL_EL_ID, LAYOUT_MAX_Z_INDEX, createLayoutCssVars } from './shared';
import { LAYOUT_MAX_Z_INDEX, LAYOUT_SCROLL_EL_ID, createLayoutCssVars } from './shared';
import style from './index.module.css';
defineOptions({
@@ -152,10 +29,8 @@ const props = withDefaults(defineProps<AdminLayoutProps>(), {
});
interface Emits {
/**
* @description: click the mask when layout mode is mobile
*/
(e: 'click-mobile-sider-mask'): void;
/** Update siderCollapse */
(e: 'update:siderCollapse', collapse: boolean): void;
}
const emit = defineEmits<Emits>();
@@ -163,17 +38,18 @@ const emit = defineEmits<Emits>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/** main */
/** Main */
default?: SlotFn;
/** header */
/** Header */
header?: SlotFn;
/** tab */
/** Tab */
tab?: SlotFn;
/** sider */
/** Sider */
sider?: SlotFn;
/** footer */
/** Footer */
footer?: SlotFn;
};
const slots = defineSlots<Slots>();
const cssVars = computed(() => createLayoutCssVars(props));
@@ -232,8 +108,130 @@ const siderPaddingClass = computed(() => {
});
function handleClickMask() {
emit('click-mobile-sider-mask');
emit('update:siderCollapse', true);
}
</script>
<template>
<div class="relative h-full" :class="[commonClass]" :style="cssVars">
<div
:id="isWrapperScroll ? scrollElId : undefined"
class="h-full flex flex-col"
:class="[commonClass, scrollWrapperClass, { 'overflow-y-auto': isWrapperScroll }]"
>
<!-- Header -->
<template v-if="showHeader">
<header
v-show="!fullContent"
class="flex-shrink-0"
:class="[
style['layout-header'],
commonClass,
headerClass,
headerLeftGapClass,
{ 'absolute top-0 left-0 w-full': fixedHeaderAndTab }
]"
>
<slot name="header"></slot>
</header>
<div
v-show="!fullContent && fixedHeaderAndTab"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-header-placement']]"
></div>
</template>
<!-- Tab -->
<template v-if="showTab">
<div
class="flex-shrink-0"
:class="[
style['layout-tab'],
commonClass,
tabClass,
{ 'top-0!': fullContent || !showHeader },
leftGapClass,
{ 'absolute left-0 w-full': fixedHeaderAndTab }
]"
>
<slot name="tab"></slot>
</div>
<div
v-show="fullContent || fixedHeaderAndTab"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-tab-placement']]"
></div>
</template>
<!-- Sider -->
<template v-if="showSider">
<aside
v-show="!fullContent"
class="absolute left-0 top-0 h-full"
:class="[
commonClass,
siderClass,
siderPaddingClass,
siderCollapse ? style['layout-sider_collapsed'] : style['layout-sider']
]"
>
<slot name="sider"></slot>
</aside>
</template>
<!-- Mobile Sider -->
<template v-if="showMobileSider">
<aside
class="absolute left-0 top-0 h-full w-0 bg-white"
:class="[
commonClass,
mobileSiderClass,
style['layout-mobile-sider'],
siderCollapse ? 'overflow-hidden' : style['layout-sider']
]"
>
<slot name="sider"></slot>
</aside>
<div
v-show="!siderCollapse"
class="absolute left-0 top-0 h-full w-full bg-[rgba(0,0,0,0.2)]"
:class="[style['layout-mobile-sider-mask']]"
@click="handleClickMask"
></div>
</template>
<!-- Main Content -->
<main
:id="isContentScroll ? scrollElId : undefined"
class="flex flex-col flex-grow"
:class="[commonClass, contentClass, leftGapClass, { 'overflow-y-auto': isContentScroll }]"
>
<slot></slot>
</main>
<!-- Footer -->
<template v-if="showFooter">
<footer
v-show="!fullContent"
class="flex-shrink-0"
:class="[
style['layout-footer'],
commonClass,
footerClass,
footerLeftGapClass,
{ 'absolute left-0 bottom-0 w-full': fixedFooter }
]"
>
<slot name="footer"></slot>
</footer>
<div
v-show="!fullContent && fixedFooter"
class="flex-shrink-0 overflow-hidden"
:class="[style['layout-footer-placement']]"
></div>
</template>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -1,18 +1,15 @@
import type { AdminLayoutProps, LayoutCssVarsProps, LayoutCssVars } from '../../types';
import type { AdminLayoutProps, LayoutCssVars, LayoutCssVarsProps } from '../../types';
/**
* the id of the scroll element of the layout
*/
/** The id of the scroll element of the layout */
export const LAYOUT_SCROLL_EL_ID = '__SCROLL_EL_ID__';
/**
* the max z-index of the layout
*/
/** The max z-index of the layout */
export const LAYOUT_MAX_Z_INDEX = 100;
/**
* create layout css vars by css vars props
* @param props css vars props
* Create layout css vars by css vars props
*
* @param props Css vars props
*/
function createLayoutCssVarsByCssVarsProps(props: LayoutCssVarsProps) {
const cssVars: LayoutCssVars = {
@@ -32,7 +29,8 @@ function createLayoutCssVarsByCssVarsProps(props: LayoutCssVarsProps) {
}
/**
* create layout css vars
* Create layout css vars
*
* @param props
*/
export function createLayoutCssVars(props: AdminLayoutProps) {

View File

@@ -1,7 +1,43 @@
<script setup lang="ts">
import type { PageTabProps } from '../../types';
import style from './index.module.css';
defineOptions({
name: 'ButtonTab'
});
defineProps<PageTabProps>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/**
* Slot
*
* The center content of the tab
*/
default?: SlotFn;
/**
* Slot
*
* The left content of the tab
*/
prefix?: SlotFn;
/**
* Slot
*
* The right content of the tab
*/
suffix?: SlotFn;
};
defineSlots<Slots>();
</script>
<template>
<div
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-12px whitespace-nowrap border-(1px solid) rounded-4px px-12px py-4px"
:class="[
':soy: relative inline-flex justify-center items-center gap-12px px-12px py-4px border-1px border-solid rounded-4px cursor-pointer whitespace-nowrap',
style['button-tab'],
{ [style['button-tab_dark']]: darkMode },
{ [style['button-tab_active']]: active },
@@ -14,36 +50,4 @@
</div>
</template>
<script setup lang="ts">
import style from './index.module.css';
import type { PageTabProps } from '../../types';
defineOptions({
name: 'ButtonTab'
});
defineProps<PageTabProps>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/**
* slot
* @description the center content of the tab
*/
default?: SlotFn;
/**
* slot
* @description the left content of the tab
*/
prefix?: SlotFn;
/**
* slot
* @description the right content of the tab
*/
suffix?: SlotFn;
};
defineSlots<Slots>();
</script>
<style scoped></style>

View File

@@ -1,31 +1,31 @@
<template>
<svg style="width: 100%; height: 100%">
<defs>
<symbol id="geometry-left" viewBox="0 0 214 36">
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z"></path>
</symbol>
<symbol id="geometry-right" viewBox="0 0 214 36">
<use xlink:href="#geometry-left"></use>
</symbol>
<clipPath>
<rect width="100%" height="100%" x="0"></rect>
</clipPath>
</defs>
<svg width="51%" height="100%">
<use xlink:href="#geometry-left" width="214" height="36" fill="currentColor"></use>
</svg>
<g transform="scale(-1, 1)">
<svg width="51%" height="100%" x="-100%" y="0">
<use xlink:href="#geometry-right" width="214" height="36" fill="currentColor"></use>
</svg>
</g>
</svg>
</template>
<script setup lang="ts">
defineOptions({
name: 'ChromeTabBg'
});
</script>
<template>
<svg class="size-full">
<defs>
<symbol id="geometry-left" viewBox="0 0 214 36">
<path d="M17 0h197v36H0v-2c4.5 0 9-3.5 9-8V8c0-4.5 3.5-8 8-8z" />
</symbol>
<symbol id="geometry-right" viewBox="0 0 214 36">
<use xlink:href="#geometry-left" />
</symbol>
<clipPath>
<rect width="100%" height="100%" x="0" />
</clipPath>
</defs>
<svg width="51%" height="100%">
<use xlink:href="#geometry-left" width="214" height="36" fill="currentColor" />
</svg>
<g transform="scale(-1, 1)">
<svg width="51%" height="100%" x="-100%" y="0">
<use xlink:href="#geometry-right" width="214" height="36" fill="currentColor" />
</svg>
</g>
</svg>
</template>
<style scoped></style>

View File

@@ -1,27 +1,7 @@
<template>
<div
:class="[
':soy: relative inline-flex justify-center items-center gap-16px -mr-18px px-24px py-6px cursor-pointer whitespace-nowrap',
style['chrome-tab'],
{ [style['chrome-tab_dark']]: darkMode },
{ [style['chrome-tab_active']]: active },
{ [style['chrome-tab_active_dark']]: active && darkMode }
]"
>
<div :class="[':soy: absolute left-0 top-0 -z-1 w-full h-full pointer-events-none', style['chrome-tab__bg']]">
<ChromeTabBg />
</div>
<slot name="prefix"></slot>
<slot></slot>
<slot name="suffix"></slot>
<div :class="[':soy: absolute right-7px w-1px h-16px bg-#1f2225', style['chrome-tab-divider']]"></div>
</div>
</template>
<script setup lang="ts">
import type { PageTabProps } from '../../types';
import ChromeTabBg from './chrome-tab-bg.vue';
import style from './index.module.css';
import type { PageTabProps } from '../../types';
defineOptions({
name: 'ChromeTab'
@@ -33,18 +13,21 @@ type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/**
* slot
* @description the center content of the tab
* Slot
*
* The center content of the tab
*/
default?: SlotFn;
/**
* slot
* @description the left content of the tab
* Slot
*
* The left content of the tab
*/
prefix?: SlotFn;
/**
* slot
* @description the right content of the tab
* Slot
*
* The right content of the tab
*/
suffix?: SlotFn;
};
@@ -52,4 +35,24 @@ type Slots = {
defineSlots<Slots>();
</script>
<template>
<div
class=":soy: relative inline-flex cursor-pointer items-center justify-center gap-16px whitespace-nowrap px-24px py-6px -mr-18px"
:class="[
style['chrome-tab'],
{ [style['chrome-tab_dark']]: darkMode },
{ [style['chrome-tab_active']]: active },
{ [style['chrome-tab_active_dark']]: active && darkMode }
]"
>
<div class=":soy: pointer-events-none absolute left-0 top-0 h-full w-full -z-1" :class="[style['chrome-tab__bg']]">
<ChromeTabBg />
</div>
<slot name="prefix"></slot>
<slot></slot>
<slot name="suffix"></slot>
<div class=":soy: absolute right-7px h-16px w-1px bg-#1f2225" :class="[style['chrome-tab-divider']]"></div>
</div>
</template>
<style scoped></style>

View File

@@ -23,13 +23,13 @@
background-color: var(--soy-primary-color-opacity2);
}
.button-tab .icon_close:hover {
.button-tab .svg-close:hover {
font-size: 12px;
color: #ffffff;
background-color: var(--soy-primary-color);
}
.button-tab_dark .icon_close:hover {
.button-tab_dark .svg-close:hover {
color: #000000;
}
@@ -70,17 +70,17 @@
color: var(--soy-primary-color2);
}
.chrome-tab .icon_close:hover {
.chrome-tab .svg-close:hover {
font-size: 12px;
color: #ffffff;
background-color: #9ca3af;
}
.chrome-tab_active .icon_close:hover {
.chrome-tab_active .svg-close:hover {
background-color: var(--soy-primary-color);
}
.chrome-tab_dark .icon_close:hover {
.chrome-tab_dark .svg-close:hover {
color: #000000;
}

View File

@@ -3,12 +3,13 @@ declare const styles: {
readonly 'button-tab_dark': string;
readonly 'button-tab_active': string;
readonly 'button-tab_active_dark': string;
readonly icon_close: string;
readonly 'chrome-tab': string;
readonly 'chrome-tab_active': string;
readonly 'chrome-tab__bg': string;
readonly 'chrome-tab_active_dark': string;
readonly 'chrome-tab_dark': string;
readonly 'chrome-tab-divider': string;
readonly 'svg-close': string;
};
export = styles;
export default styles;

View File

@@ -1,26 +1,12 @@
<template>
<component :is="activeTabComponent.component" :class="activeTabComponent.class" :style="cssVars" v-bind="bindProps">
<template #prefix>
<slot name="prefix"></slot>
</template>
<slot></slot>
<template #suffix>
<slot name="suffix">
<IconClose v-if="closable" :class="[style['icon_close']]" @click="handleClose" />
</slot>
</template>
</component>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
import { createTabCssVars, ACTIVE_COLOR } from './shared';
import type { PageTabMode, PageTabProps } from '../../types';
import { ACTIVE_COLOR, createTabCssVars } from './shared';
import ChromeTab from './chrome-tab.vue';
import ButtonTab from './button-tab.vue';
import IconClose from './icon-close.vue';
import SvgClose from './svg-close.vue';
import style from './index.module.css';
import type { PageTabProps, PageTabMode } from '../../types';
defineOptions({
name: 'PageTab'
@@ -39,28 +25,6 @@ interface Emits {
const emit = defineEmits<Emits>();
type SlotFn = (props?: Record<string, unknown>) => any;
type Slots = {
/**
* slot
* @description the center content of the tab
*/
default?: SlotFn;
/**
* slot
* @description the left content of the tab
*/
prefix?: SlotFn;
/**
* slot
* @description the right content of the tab
*/
suffix?: SlotFn;
};
defineSlots<Slots>();
const activeTabComponent = computed(() => {
const { mode, chromeClass, buttonClass } = props;
@@ -91,4 +55,18 @@ function handleClose() {
}
</script>
<template>
<component :is="activeTabComponent.component" :class="activeTabComponent.class" :style="cssVars" v-bind="bindProps">
<template #prefix>
<slot name="prefix"></slot>
</template>
<slot></slot>
<template #suffix>
<slot name="suffix">
<SvgClose v-if="closable" :class="[style['svg-close']]" @click.stop="handleClose" />
</slot>
</template>
</component>
</template>
<style scoped></style>

View File

@@ -1,9 +1,7 @@
import { addColorAlpha, transformColorWithOpacity } from '@sa/utils';
import type { PageTabCssVarsProps, PageTabCssVars } from '../../types';
import { addColorAlpha, transformColorWithOpacity } from '@sa/color';
import type { PageTabCssVars, PageTabCssVarsProps } from '../../types';
/**
* the active color of the tab
*/
/** The active color of the tab */
export const ACTIVE_COLOR = '#1890ff';
function createCssVars(props: PageTabCssVarsProps) {

View File

@@ -1,31 +1,18 @@
<script setup lang="ts">
defineOptions({
name: 'SvgClose'
});
</script>
<template>
<div
class=":soy: relative inline-flex justify-center items-center w-16px h-16px text-14px rd-50%"
@click.stop="handleClick"
>
<div class=":soy: relative h-16px w-16px inline-flex items-center justify-center rd-50% text-14px">
<svg width="1em" height="1em" viewBox="0 0 1024 1024">
<path
fill="currentColor"
d="m563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8L295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512L196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1l216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
></path>
/>
</svg>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'IconClose'
});
interface Emits {
(e: 'click'): void;
}
const emit = defineEmits<Emits>();
function handleClick() {
emit('click');
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,3 @@
import SimpleScrollbar from './index.vue';
export default SimpleScrollbar;

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import Simplebar from 'simplebar-vue';
import 'simplebar-vue/dist/simplebar.min.css';
defineOptions({
name: 'SimpleScrollbar'
});
</script>
<template>
<div class="h-full flex-1-hidden">
<Simplebar class="h-full">
<slot />
</Simplebar>
</div>
</template>
<style scoped></style>

View File

@@ -1,18 +0,0 @@
<script setup lang="ts">
defineOptions({
name: 'Tooltip'
});
</script>
<template>
<div class="relative">
<slot></slot>
<div
class="absolute z-10 hidden w-32 p-2 text-xs text-white bg-gray-800 rounded shadow-lg top-0 left-0 -mt-8 -ml-8"
>
<slot name="content"></slot>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -1,146 +1,156 @@
/**
* header config
*/
/** Header config */
interface AdminLayoutHeaderConfig {
/**
* whether header is visible
* Whether header is visible
*
* @default true
*/
headerVisible?: boolean;
/**
* header class
* Header class
*
* @default ''
*/
headerClass?: string;
/**
* header height
* Header height
*
* @default 56px
*/
headerHeight?: number;
}
/**
* tab config
*/
/** Tab config */
interface AdminLayoutTabConfig {
/**
* whether tab is visible
* Whether tab is visible
*
* @default true
*/
tabVisible?: boolean;
/**
* tab class
* Tab class
*
* @default ''
*/
tabClass?: string;
/**
* tab height
* Tab height
*
* @default 48px
*/
tabHeight?: number;
}
/**
* sider config
*/
/** Sider config */
interface AdminLayoutSiderConfig {
/**
* whether sider is visible
* Whether sider is visible
*
* @default true
*/
siderVisible?: boolean;
/**
* sider class
* Sider class
*
* @default ''
*/
siderClass?: string;
/**
* mobile sider class
* Mobile sider class
*
* @default ''
*/
mobileSiderClass?: string;
/**
* sider collapse status
* Sider collapse status
*
* @default false
*/
siderCollapse?: boolean;
/**
* sider width when collapse is false
* Sider width when collapse is false
*
* @default '220px'
*/
siderWidth?: number;
/**
* sider width when collapse is true
* Sider width when collapse is true
*
* @default '64px'
*/
siderCollapsedWidth?: number;
}
/**
* content config
*/
/** Content config */
export interface AdminLayoutContentConfig {
/**
* content class
* Content class
*
* @default ''
*/
contentClass?: string;
/**
* whether content is full the page
* @description if true, other elements will be hidden by `display: none`
* Whether content is full the page
*
* If true, other elements will be hidden by `display: none`
*/
fullContent?: boolean;
}
/**
* footer config
*/
/** Footer config */
export interface AdminLayoutFooterConfig {
/**
* whether footer is visible
* Whether footer is visible
*
* @default true
*/
footerVisible?: boolean;
/**
* whether footer is fixed
* Whether footer is fixed
*
* @default true
*/
fixedFooter?: boolean;
/**
* footer class
* Footer class
*
* @default ''
*/
footerClass?: string;
/**
* footer height
* Footer height
*
* @default 48px
*/
footerHeight?: number;
/**
* whether footer is on the right side
* @description when the layout is vertical, the footer is on the right side
* Whether footer is on the right side
*
* When the layout is vertical, the footer is on the right side
*/
rightFooter?: boolean;
}
/**
* layout mode
* - horizontal
* - vertical
* Layout mode
*
* - Horizontal
* - Vertical
*/
export type LayoutMode = 'horizontal' | 'vertical';
/**
* the scroll mode when content overflow
* - wrapper: the layout component's wrapper element has a scrollbar
* - content: the layout component's content element has a scrollbar
* The scroll mode when content overflow
*
* - Wrapper: the layout component's wrapper element has a scrollbar
* - Content: the layout component's content element has a scrollbar
*
* @default 'wrapper'
*/
export type LayoutScrollMode = 'wrapper' | 'content';
/**
* admin layout props
*/
/** Admin layout props */
export interface AdminLayoutProps
extends AdminLayoutHeaderConfig,
AdminLayoutTabConfig,
@@ -148,52 +158,58 @@ export interface AdminLayoutProps
AdminLayoutContentConfig,
AdminLayoutFooterConfig {
/**
* layout mode
* Layout mode
*
* - {@link LayoutMode}
*/
mode?: LayoutMode;
/** is mobile layout */
/** Is mobile layout */
isMobile?: boolean;
/**
* scroll mode
* Scroll mode
*
* - {@link ScrollMode}
*/
scrollMode?: LayoutScrollMode;
/**
* the id of the scroll element of the layout
* @description it can be used to get the corresponding Dom and scroll it
* The id of the scroll element of the layout
*
* It can be used to get the corresponding Dom and scroll it
*
* @example
* use the default id by import
* ```ts
* import { adminLayoutScrollElId } from '@sa/vue-materials';
* ```
*
* @default
* ```ts
* const adminLayoutScrollElId = '__ADMIN_LAYOUT_SCROLL_EL_ID__'
* ```
* @example use the default id by import
* ```ts
* import { adminLayoutScrollElId } from '@sa/vue-materials';
* ```
*/
scrollElId?: string;
/**
* the class of the scroll element
*/
/** The class of the scroll element */
scrollElClass?: string;
/**
* the class of the scroll wrapper element
*/
/** The class of the scroll wrapper element */
scrollWrapperClass?: string;
/**
* the common class of the layout
* @description is can be used to configure the transition animation
* The common class of the layout
*
* Is can be used to configure the transition animation
*
* @default 'transition-all-300'
*/
commonClass?: string;
/**
* whether fix the header and tab
* Whether fix the header and tab
*
* @default true
*/
fixedTop?: boolean;
/**
* the max z-index of the layout
* @description the z-index of Header,Tab,Sider and Footer will not exceed this value
* The max z-index of the layout
*
* The z-index of Header,Tab,Sider and Footer will not exceed this value
*/
maxZIndex?: number;
}
@@ -222,48 +238,44 @@ export type LayoutCssVars = {
};
/**
* the mode of the tab
* - button: button style
* - chrome: chrome style
* The mode of the tab
*
* - Button: button style
* - Chrome: chrome style
*
* @default chrome
*/
export type PageTabMode = 'button' | 'chrome';
export interface PageTabProps {
/**
* whether is dark mode
*/
/** Whether is dark mode */
darkMode?: boolean;
/**
* the mode of the tab
* The mode of the tab
*
* - {@link TabMode}
*/
mode?: PageTabMode;
/**
* the common class of the layout
* @description is can be used to configure the transition animation
* The common class of the layout
*
* Is can be used to configure the transition animation
*
* @default 'transition-all-300'
*/
commonClass?: string;
/**
* the class of the button tab
*/
/** The class of the button tab */
buttonClass?: string;
/**
* the class of the chrome tab
*/
/** The class of the chrome tab */
chromeClass?: string;
/**
* whether the tab is active
*/
/** Whether the tab is active */
active?: boolean;
/**
* the color of the active tab
*/
/** The color of the active tab */
activeColor?: string;
/**
* whether the tab is closable
* @description show the close icon when true
* Whether the tab is closable
*
* Show the close icon when true
*/
closable?: boolean;
}

View File

@@ -1,19 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"target": "ESNext",
"lib": ["DOM", "ESNext"],
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"jsx": "preserve",
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"types": ["node"]
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]

View File

@@ -1,17 +1,15 @@
{
"name": "@sa/request",
"version": "1.0.0",
"name": "@sa/fetch",
"version": "1.3.7",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": [
"./src/*"
]
"*": ["./src/*"]
}
},
"dependencies": {
"ofetch": "1.3.3"
"ofetch": "1.4.1"
}
}

View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env node
const path = require('path');
const jiti = require('jiti')(__filename);
jiti(path.resolve(__dirname, './src/index.ts'));

3
packages/scripts/bin.ts Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env tsx
import './src/index.ts';

View File

@@ -1,28 +1,27 @@
{
"name": "@sa/scripts",
"version": "1.0.0",
"version": "1.3.7",
"bin": {
"sa": "./bin.ts"
},
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": [
"./src/*"
]
"*": ["./src/*"]
}
},
"bin": {
"sa": "./bin.cjs"
},
"devDependencies": {
"c12": "1.5.1",
"@soybeanjs/changelog": "0.3.24",
"bumpp": "9.7.1",
"c12": "2.0.1",
"cac": "6.7.14",
"consola": "3.2.3",
"enquirer": "2.4.1",
"execa": "8.0.1",
"jiti": "1.20.0",
"lint-staged": "15.0.2",
"npm-check-updates": "16.14.6",
"rimraf": "5.0.5"
"execa": "9.4.0",
"kolorist": "1.8.0",
"npm-check-updates": "17.1.3",
"rimraf": "6.0.1"
}
}

View File

@@ -0,0 +1,10 @@
import { generateChangelog, generateTotalChangelog } from '@soybeanjs/changelog';
import type { ChangelogOption } from '@soybeanjs/changelog';
export async function genChangelog(options?: Partial<ChangelogOption>, total = false) {
if (total) {
await generateTotalChangelog(options);
} else {
await generateChangelog(options);
}
}

View File

@@ -1,9 +1,9 @@
import path from 'node:path';
import { readFileSync } from 'node:fs';
import enquirer from 'enquirer';
import { bgRed, red, green } from 'kolorist';
import { prompt } from 'enquirer';
import { execCommand } from '../shared';
import type { CliOption } from '../types';
import { locales } from '../locales';
import type { Lang } from '../locales';
interface PromptObject {
types: string;
@@ -11,65 +11,74 @@ interface PromptObject {
description: string;
}
export async function gitCommit(
gitCommitTypes: CliOption['gitCommitTypes'],
gitCommitScopes: CliOption['gitCommitScopes']
) {
const typesChoices = gitCommitTypes.map(([name, title]) => {
const nameWithSuffix = `${name}:`;
/**
* Git commit with Conventional Commits standard
*
* @param lang
*/
export async function gitCommit(lang: Lang = 'en-us') {
const { gitCommitMessages, gitCommitTypes, gitCommitScopes } = locales[lang];
const message = `${nameWithSuffix.padEnd(12)}${title}`;
const typesChoices = gitCommitTypes.map(([value, msg]) => {
const nameWithSuffix = `${value}:`;
const message = `${nameWithSuffix.padEnd(12)}${msg}`;
return {
name,
name: value,
message
};
});
const scopesChoices = gitCommitScopes.map(([name, title]) => ({
name,
message: `${name.padEnd(30)} (${title})`
const scopesChoices = gitCommitScopes.map(([value, msg]) => ({
name: value,
message: `${value.padEnd(30)} (${msg})`
}));
const result = await enquirer.prompt<PromptObject>([
const result = await prompt<PromptObject>([
{
name: 'types',
type: 'select',
message: 'Please select a type',
message: gitCommitMessages.types,
choices: typesChoices
},
{
name: 'scopes',
type: 'select',
message: 'Please select a scope',
message: gitCommitMessages.scopes,
choices: scopesChoices
},
{
name: 'description',
type: 'text',
message: 'Please enter a description'
message: gitCommitMessages.description
}
]);
const commitMsg = `${result.types}(${result.scopes}): ${result.description}`;
const breaking = result.description.startsWith('!') ? '!' : '';
const description = result.description.replace(/^!/, '').trim();
const commitMsg = `${result.types}(${result.scopes})${breaking}: ${description}`;
await execCommand('git', ['commit', '-m', commitMsg], { stdio: 'inherit' });
}
export async function gitCommitVerify() {
/** Git commit message verify */
export async function gitCommitVerify(lang: Lang = 'en-us', ignores: RegExp[] = []) {
const gitPath = await execCommand('git', ['rev-parse', '--show-toplevel']);
const gitMsgPath = path.join(gitPath, '.git', 'COMMIT_EDITMSG');
const commitMsg = readFileSync(gitMsgPath, 'utf8').trim();
const REG_EXP = /(?<type>[a-z]+)(\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
if (ignores.some(regExp => regExp.test(commitMsg))) return;
const REG_EXP = /(?<type>[a-z]+)(?:\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
if (!REG_EXP.test(commitMsg)) {
throw new Error(
`${bgRed(' ERROR ')} ${red('git commit message must match the Conventional Commits standard!')}\n\n${green(
'Recommended to use the command `pnpm commit` to generate Conventional Commits compliant commit information.\nGet more info about Conventional Commits, follow this link: https://conventionalcommits.org'
)}`
);
const errorMsg = locales[lang].gitCommitVerify;
throw new Error(errorMsg);
}
}

View File

@@ -1,5 +1,6 @@
export * from './git-commit';
export * from './cleanup';
export * from './update-pkg';
export * from './prettier';
export * from './lint-staged';
export * from './changelog';
export * from './release';
export * from './router';

View File

@@ -1,5 +0,0 @@
export async function execLintStaged(config: Record<string, string | string[]>) {
const lintStaged = (await import('lint-staged')).default;
return lintStaged({ config, allowEmpty: true });
}

View File

@@ -1,7 +0,0 @@
import { execCommand } from '../shared';
export async function prettierWrite(writeGlob: string[]) {
await execCommand('npx', ['prettier', '--write', '.', ...writeGlob], {
stdio: 'inherit'
});
}

View File

@@ -0,0 +1,12 @@
import { versionBump } from 'bumpp';
export async function release(execute = 'pnpm sa changelog', push = true) {
await versionBump({
files: ['**/package.json', '!**/node_modules'],
execute,
all: true,
tag: true,
commit: 'chore(projects): release v%s',
push
});
}

View File

@@ -0,0 +1,90 @@
import process from 'node:process';
import path from 'node:path';
import { writeFile } from 'node:fs/promises';
import { existsSync, mkdirSync } from 'node:fs';
import { prompt } from 'enquirer';
import { green, red } from 'kolorist';
interface PromptObject {
routeName: string;
addRouteParams: boolean;
routeParams: string;
}
/** generate route */
export async function generateRoute() {
const result = await prompt<PromptObject>([
{
name: 'routeName',
type: 'text',
message: 'please enter route name',
initial: 'demo-route_child'
},
{
name: 'addRouteParams',
type: 'confirm',
message: 'add route params?',
initial: false
}
]);
if (result.addRouteParams) {
const answers = await prompt<PromptObject>({
name: 'routeParams',
type: 'text',
message: 'please enter route params',
initial: 'id'
});
Object.assign(result, answers);
}
const PAGE_DIR_NAME_PATTERN = /^[\w-]+[0-9a-zA-Z]+$/;
if (!PAGE_DIR_NAME_PATTERN.test(result.routeName)) {
throw new Error(`${red('route name is invalid, it only allow letters, numbers, "-" or "_"')}.
For example:
(1) one level route: ${green('demo-route')}
(2) two level route: ${green('demo-route_child')}
(3) multi level route: ${green('demo-route_child_child')}
(4) group route: ${green('_ignore_demo-route')}'
`);
}
const PARAM_REG = /^\w+$/g;
if (result.routeParams && !PARAM_REG.test(result.routeParams)) {
throw new Error(red('route params is invalid, it only allow letters, numbers or "_".'));
}
const cwd = process.cwd();
const [dir, ...rest] = result.routeName.split('_') as string[];
let routeDir = path.join(cwd, 'src', 'views', dir);
if (rest.length) {
routeDir = path.join(routeDir, rest.join('_'));
}
if (!existsSync(routeDir)) {
mkdirSync(routeDir, { recursive: true });
} else {
throw new Error(red('route already exists'));
}
const fileName = result.routeParams ? `[${result.routeParams}].vue` : 'index.vue';
const vueTemplate = `<script setup lang="ts"></script>
<template>
<div>${result.routeName}</div>
</template>
<style scoped></style>
`;
const filePath = path.join(routeDir, fileName);
await writeFile(filePath, vueTemplate);
}

View File

@@ -1,8 +1,7 @@
import process from 'node:process';
import { loadConfig } from 'c12';
import type { CliOption } from '../types';
const eslintExt = '*.{js,jsx,mjs,cjs,ts,tsx,vue}';
const defaultOptions: CliOption = {
cwd: process.cwd(),
cleanupDirs: [
@@ -13,52 +12,18 @@ const defaultOptions: CliOption = {
'**/node_modules',
'!node_modules/**'
],
gitCommitTypes: [
['feat', 'A new feature'],
['fix', 'A bug fix'],
['docs', 'Documentation only changes'],
['style', 'Changes that do not affect the meaning of the code'],
['refactor', 'A code change that neither fixes a bug nor adds a feature'],
['perf', 'A code change that improves performance'],
['test', 'Adding missing tests or correcting existing tests'],
['build', 'Changes that affect the build system or external dependencies'],
['ci', 'Changes to our CI configuration files and scripts'],
['chore', "Other changes that don't modify src or test files"],
['revert', 'Reverts a previous commit']
],
gitCommitScopes: [
['projects', 'project'],
['components', 'components'],
['hooks', 'hook functions'],
['utils', 'utils functions'],
['types', 'TS declaration'],
['styles', 'style'],
['deps', 'project dependencies'],
['release', 'release project'],
['other', 'other changes']
],
ncuCommandArgs: ['--deep', '-u'],
prettierWriteGlob: [
`!**/${eslintExt}`,
'!*.min.*',
'!CHANGELOG.md',
'!dist',
'!LICENSE*',
'!output',
'!coverage',
'!public',
'!temp',
'!package-lock.json',
'!pnpm-lock.yaml',
'!yarn.lock',
'!.github',
'!__snapshots__',
'!node_modules'
],
lintStagedConfig: {
[eslintExt]: 'eslint --fix',
'*': 'sa prettier-write'
}
changelogOptions: {},
gitCommitVerifyIgnores: [
/^((Merge pull request)|(Merge (.*?) into (.*?)|(Merge branch (.*?)))(?:\r?\n)*$)/m,
/^(Merge tag (.*?))(?:\r?\n)*$/m,
/^(R|r)evert (.*)/,
/^(amend|fixup|squash)!/,
/^(Merged (.*?)(in|into) (.*)|Merged PR (.*): (.*))/,
/^Merge remote-tracking branch(\s*)(.*)/,
/^Automatic merge(.*)/,
/^Auto-merged (.*?) into (.*)/
]
};
export async function loadCliOptions(overrides?: Partial<CliOption>, cwd = process.cwd()) {

View File

@@ -1,25 +1,58 @@
import cac from 'cac';
import { blue, lightGreen } from 'kolorist';
import { version } from '../package.json';
import { cleanup, updatePkg, gitCommit, gitCommitVerify, prettierWrite, execLintStaged } from './commands';
import { cleanup, genChangelog, generateRoute, gitCommit, gitCommitVerify, release, updatePkg } from './commands';
import { loadCliOptions } from './config';
import type { Lang } from './locales';
type Command = 'cleanup' | 'update-pkg' | 'git-commit' | 'git-commit-verify' | 'prettier-write' | 'lint-staged';
type Command = 'cleanup' | 'update-pkg' | 'git-commit' | 'git-commit-verify' | 'changelog' | 'release' | 'gen-route';
type CommandAction<A extends object> = (args?: A) => Promise<void> | void;
type CommandWithAction<A extends object = object> = Record<Command, { desc: string; action: CommandAction<A> }>;
interface CommandArg {
/** Execute additional command after bumping and before git commit. Defaults to 'pnpm sa changelog' */
execute?: string;
/** Indicates whether to push the git commit and tag. Defaults to true */
push?: boolean;
/** Generate changelog by total tags */
total?: boolean;
/**
* The glob pattern of dirs to clean up
*
* If not set, it will use the default value
*
* Multiple values use "," to separate them
*/
cleanupDir?: string;
/**
* display lang of cli
*
* @default 'en-us'
*/
lang?: Lang;
}
export async function setupCli() {
const cliOptions = await loadCliOptions();
const cli = cac(blue('soybean'));
const cli = cac(blue('soybean-admin'));
cli.version(lightGreen(version)).help();
cli
.version(lightGreen(version))
.option(
'-e, --execute [command]',
"Execute additional command after bumping and before git commit. Defaults to 'npx soy changelog'"
)
.option('-p, --push', 'Indicates whether to push the git commit and tag')
.option('-t, --total', 'Generate changelog by total tags')
.option(
'-c, --cleanupDir <dir>',
'The glob pattern of dirs to cleanup, If not set, it will use the default value, Multiple values use "," to separate them'
)
.option('-l, --lang <lang>', 'display lang of cli', { default: 'en-us', type: [String] })
.help();
const commands: CommandWithAction<CommandArg> = {
cleanup: {
@@ -36,26 +69,32 @@ export async function setupCli() {
},
'git-commit': {
desc: 'git commit, generate commit message which match Conventional Commits standard',
action: async () => {
await gitCommit(cliOptions.gitCommitTypes, cliOptions.gitCommitScopes);
action: async args => {
await gitCommit(args?.lang);
}
},
'git-commit-verify': {
desc: 'verify git commit message, make sure it match Conventional Commits standard',
action: async () => {
await gitCommitVerify();
action: async args => {
await gitCommitVerify(args?.lang, cliOptions.gitCommitVerifyIgnores);
}
},
'prettier-write': {
desc: 'run prettier --write',
action: async () => {
await prettierWrite(cliOptions.prettierWriteGlob);
changelog: {
desc: 'generate changelog',
action: async args => {
await genChangelog(cliOptions.changelogOptions, args?.total);
}
},
'lint-staged': {
desc: 'run lint-staged',
release: {
desc: 'release: update version, generate changelog, commit code',
action: async args => {
await release(args?.execute, args?.push);
}
},
'gen-route': {
desc: 'generate route',
action: async () => {
await execLintStaged(cliOptions.lintStagedConfig);
await generateRoute();
}
}
};

View File

@@ -0,0 +1,82 @@
import { bgRed, green, red, yellow } from 'kolorist';
export type Lang = 'zh-cn' | 'en-us';
export const locales = {
'zh-cn': {
gitCommitMessages: {
types: '请选择提交类型',
scopes: '请选择提交范围',
description: `请输入描述信息(${yellow('!')}开头表示破坏性改动`
},
gitCommitTypes: [
['feat', '新功能'],
['feat-wip', '开发中的功能,比如某功能的部分代码'],
['fix', '修复Bug'],
['docs', '只涉及文档更新'],
['typo', '代码或文档勘误,比如错误拼写'],
['style', '修改代码风格,不影响代码含义的变更'],
['refactor', '代码重构,既不修复 bug 也不添加功能的代码变更'],
['perf', '可提高性能的代码更改'],
['optimize', '优化代码质量的代码更改'],
['test', '添加缺失的测试或更正现有测试'],
['build', '影响构建系统或外部依赖项的更改'],
['ci', '对 CI 配置文件和脚本的更改'],
['chore', '没有修改src或测试文件的其他变更'],
['revert', '还原先前的提交']
] as [string, string][],
gitCommitScopes: [
['projects', '项目'],
['packages', '包'],
['components', '组件'],
['hooks', '钩子函数'],
['utils', '工具函数'],
['types', 'TS类型声明'],
['styles', '代码风格'],
['deps', '项目依赖'],
['release', '发布项目新版本'],
['other', '其他的变更']
] as [string, string][],
gitCommitVerify: `${bgRed(' 错误 ')} ${red('git 提交信息必须符合 Conventional Commits 标准!')}\n\n${green(
'推荐使用命令 `pnpm commit` 生成符合 Conventional Commits 标准的提交信息。\n获取有关 Conventional Commits 的更多信息,请访问此链接: https://conventionalcommits.org'
)}`
},
'en-us': {
gitCommitMessages: {
types: 'Please select a type',
scopes: 'Please select a scope',
description: `Please enter a description (add prefix ${yellow('!')} to indicate breaking change)`
},
gitCommitTypes: [
['feat', 'A new feature'],
['feat-wip', 'Features in development, such as partial code for a certain feature'],
['fix', 'A bug fix'],
['docs', 'Documentation only changes'],
['typo', 'Code or document corrections, such as spelling errors'],
['style', 'Changes that do not affect the meaning of the code'],
['refactor', 'A code change that neither fixes a bug nor adds a feature'],
['perf', 'A code change that improves performance'],
['optimize', 'A code change that optimizes code quality'],
['test', 'Adding missing tests or correcting existing tests'],
['build', 'Changes that affect the build system or external dependencies'],
['ci', 'Changes to our CI configuration files and scripts'],
['chore', "Other changes that don't modify src or test files"],
['revert', 'Reverts a previous commit']
] as [string, string][],
gitCommitScopes: [
['projects', 'project'],
['packages', 'packages'],
['components', 'components'],
['hooks', 'hook functions'],
['utils', 'utils functions'],
['types', 'TS declaration'],
['styles', 'style'],
['deps', 'project dependencies'],
['release', 'release project'],
['other', 'other changes']
] as [string, string][],
gitCommitVerify: `${bgRed(' ERROR ')} ${red('git commit message must match the Conventional Commits standard!')}\n\n${green(
'Recommended to use the command `pnpm commit` to generate Conventional Commits compliant commit information.\nGet more info about Conventional Commits, follow this link: https://conventionalcommits.org'
)}`
}
} satisfies Record<Lang, Record<string, unknown>>;

View File

@@ -3,5 +3,5 @@ import type { Options } from 'execa';
export async function execCommand(cmd: string, args: string[], options?: Options) {
const { execa } = await import('execa');
const res = await execa(cmd, args, options);
return res?.stdout?.trim() || '';
return (res?.stdout as string)?.trim() || '';
}

View File

@@ -1,37 +1,31 @@
import type { ChangelogOption } from '@soybeanjs/changelog';
export interface CliOption {
/**
* the project root directory
*/
/** The project root directory */
cwd: string;
/**
* cleanup dirs
* Cleanup dirs
*
* Glob pattern syntax {@link https://github.com/isaacs/minimatch}
*
* @default
* ```json
* ["** /dist", "** /pnpm-lock.yaml", "** /node_modules", "!node_modules/**"]
* ```
* @description glob pattern syntax {@link https://github.com/isaacs/minimatch}
*/
cleanupDirs: string[];
/**
* git commit types
*/
gitCommitTypes: [string, string][];
/**
* git commit scopes
*/
gitCommitScopes: [string, string][];
/**
* npm-check-updates command args
* @default ["--deep","-u"]
* Npm-check-updates command args
*
* @default ['--deep', '-u']
*/
ncuCommandArgs: string[];
/**
* prettier write glob
* @description glob pattern syntax {@link https://github.com/micromatch/micromatch}
* Options of generate changelog
*
* @link https://github.com/soybeanjs/changelog
*/
prettierWriteGlob: string[];
/**
* lint-staged config
*/
lintStagedConfig: Record<string, string | string[]>;
changelogOptions: Partial<ChangelogOption>;
/** The ignore pattern list of git commit verify */
gitCommitVerifyIgnores: RegExp[];
}

View File

@@ -1,19 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"jsx": "preserve",
"lib": ["DOM", "ESNext"],
"baseUrl": ".",
"module": "ESNext",
"target": "ESNext",
"lib": ["DOM", "ESNext"],
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"jsx": "preserve",
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": true,
"types": ["node"],
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"types": ["node"]
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*", "typings/**/*"],
"exclude": ["node_modules", "dist"]

View File

@@ -1,15 +0,0 @@
declare module 'lint-staged' {
interface LintStagedOptions {
config?: Record<string, string | string[]>;
allowEmpty?: boolean;
}
type LintStagedFn = (options: LintStagedOptions) => Promise<boolean>;
interface LintStaged extends LintStagedFn {
default: LintStagedFn;
}
const lintStaged: LintStaged;
export default lintStaged;
}

View File

@@ -1,14 +1,12 @@
{
"name": "@sa/uno-preset",
"version": "1.0.0",
"version": "1.3.7",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": [
"./src/*"
]
"*": ["./src/*"]
}
}
}

View File

@@ -5,23 +5,21 @@ import type { Theme } from '@unocss/preset-uno';
export function presetSoybeanAdmin(): Preset<Theme> {
const preset: Preset<Theme> = {
name: 'preset-soybean-tool',
name: 'preset-soybean-admin',
shortcuts: [
{
'wh-full': 'w-full h-full'
},
{
'flex-center': 'flex justify-center items-center',
'flex-x-center': 'flex justify-center',
'flex-y-center': 'flex items-center',
'flex-vertical': 'flex flex-col',
'flex-vertical-center': 'flex-center flex-col',
'flex-vertical-stretch': 'flex-vertical items-stretch',
'flex-col': 'flex flex-col',
'flex-col-center': 'flex-center flex-col',
'flex-col-stretch': 'flex-col items-stretch',
'i-flex-center': 'inline-flex justify-center items-center',
'i-flex-x-center': 'inline-flex justify-center',
'i-flex-y-center': 'inline-flex items-center',
'i-flex-vertical': 'inline-flex flex-col',
'i-flex-vertical-stretch': 'i-flex-vertical items-stretch',
'i-flex-col': 'flex-col inline-flex',
'i-flex-col-center': 'flex-col i-flex-center',
'i-flex-col-stretch': 'i-flex-col items-stretch',
'flex-1-hidden': 'flex-1 overflow-hidden'
},
{
@@ -33,7 +31,7 @@ export function presetSoybeanAdmin(): Preset<Theme> {
'absolute-tr': 'absolute-rt',
'absolute-bl': 'absolute-lb',
'absolute-br': 'absolute-rb',
'absolute-center': 'absolute-lt flex-center wh-full',
'absolute-center': 'absolute-lt flex-center size-full',
'fixed-lt': 'fixed left-0 top-0',
'fixed-lb': 'fixed left-0 bottom-0',
'fixed-rt': 'fixed right-0 top-0',
@@ -42,7 +40,7 @@ export function presetSoybeanAdmin(): Preset<Theme> {
'fixed-tr': 'fixed-rt',
'fixed-bl': 'fixed-lb',
'fixed-br': 'fixed-rb',
'fixed-center': 'fixed-lt flex-center wh-full'
'fixed-center': 'fixed-lt flex-center size-full'
},
{
'nowrap-hidden': 'overflow-hidden whitespace-nowrap',

Some files were not shown because too many files have changed in this diff Show More